你或许也想拥有专属于自己的AI模型文件格式-(3)

        如果读者没有阅读过前两篇,或者想要再次回顾,以下是对应的链接:

你或许也想拥有专属于自己的AI模型文件格式-(1)https://blog.csdn.net/Pengcode/article/details/121754272你或许也想拥有专属于自己的AI模型文件格式-(2)https://blog.csdn.net/Pengcode/article/details/121776674        前两篇文章,讲述了制作专属于自己的AI模型文件格式的初衷,写下了我们的需求,同时我们制定了相应的计划;之后,我们在第一篇中就已经开始着手落实相应计划,目前已经把环境准备、模型文件数据协议制定(使用Flatbuffers编写了schema文件,完成了定义专ai模的数据结构定义)。

        那么这篇文章呢,就开始进入到较为正式的编码阶段了,我将尽可能详尽地说明整个编码流程。如果要为这次文章的目的做个说明的,那么其实这次的目的就是利用前面已经完成的工作,编写相应的代码,生成我们的专ai模的第一个模型,也就是这次将会生成一个真正意义上的完全自己定义的模型文件了。

        那么就开始着手干活吧!

一、整体规划

        要知道,“生成我们的专ai模的第一个模型”,这件事可不仅仅是说,用代码随便写下就好了。毕竟可能我们要预料到若干年后,可能我们的专ai模变成了如今现在Caffe的这种地位(hhhhhh)!我们需要考虑到接口友好性、便利性、扩展性,另外还比较重要的就是规范性。为了更好地说明以上几种特性在本次工程目标的含义,其罗列如下所示:

名称对应含义
接口友好性编写的程序接口需要通俗易懂、传参出参要尽可能简单、不仅要自己能够看懂,也要让别人能够轻松使用。
便利性主要是追求能够用较少的代码,就能够构建出一个模型文件出来。
扩展性需要提供出非常泛化的接口,让不同人的不同需求都可以得到满足。
规范性过于灵活的接口,将会导致更加不规范的使用方式;为了规范化,我们需要在提供的接口中编写较多判断逻辑。

        既然我们确定了以上的目标,那么自然对于接下来的工作有了些许的眉目。结合之前文章的工作,我们制定了如下的工作流程:

序号计划目的
1使用FlatBuffers对上次编写的schema文件进行编译,生成代码        得到目标平台的专ai模的编程接口
2编写json文件规范并且描述出一份个性化的网络层描述采用json规范网络层描述、兼顾了灵活和规范化的要求
3寻找json文件的读取方式,从网络上下载相应的库为了第2步编写的json能够在代码中解析使用
4正式编写代码基于上面几个步骤得到的接口和库,正式编写专ai模的构建接口
5利用第4步自己编写的接口,编写例子构建我们的第一个模型文件验证第4步的编码正确无误

二、正式编写代码前准备

        为了篇幅布局合理,这里主要进行工作流程中的前面3个步骤。这三个流程较为轻松,可放心阅读。

2.1、使用FlatBuffers对schema进行代码生成

        schema文件已经在之前的文章定义完成了,那么其实这里我们只要知道了如何使用Flatc工具即可(Flatc工具就是FlatBuffers安装后的主要可执行程序)。

        之前文章定义的专ai模的数据交换协议定义出来的schema文件,被我保存为了文件名为pzk-schema.fbs的文件,其在当前目录model-flatbuffer下,如下所示:

$ ls model-flatbuffer
pzk-schema.fbs

        为了保存flatc根据pzk-schema.fbs生成的代码,我创建了include文件夹:

$ mkdir include

        根据如下所示的指令使用flatc工具进行代码生成,将会在include下看到生成的代码,其名为pzk-schema_generated.h,如下所示:

$ flatc -c -o include/  model-flatbuffer/pzk-schema.fbs
$ ls include/
pzk-schema_generated.h

2.2、编写json规范化文档

        我之前也挺疑惑我为何要再用json来约束专ai模的模型构建,因为按照schema描述,任何的网络层,任何的参数都可以直接写入到模型中,不管离谱与否,不管复杂度如何。但是后来转念一想,这样的灵活度对于用户来说反而是累坠,因为很多时间存在这种情况:用户之前可以从最基础的属性来设置这个层,写了大段的代码来描述该层的操作,这次使用完了,但是后来用户还想要再构建这种类型的网络层,我想用户是不会再想再次构建所有的属性了(因为那样的体验非常糟糕)。

       我想要让用户体验更好,这就是我编写json规范化文档的初衷,也是我后面编码的重要关注点。所以我创建了如下的json规范文档,为了缩短篇幅,我只是展示了卷积层的描述,如下所示:

[
    // 前面还有其他的层描述
    /* <---------卷积层的元描述开始------------> */
    {
      //"name"对应的值是表示层的类型,这里是卷积
      "name": "Convolution2dLayer", 
      //"category"对应的值表示该段描述描述的类型,这里表示描述的是层组成
      "category": "Layer",
      //"attributes"对应的是一个列表,表示这种类型的层拥有的属性,以及属性对应的数据类型
      "attributes": [
        { "name": "padTop", "type": "uint32" },
        { "name": "padRight", "type": "uint32" },
        { "name": "padBottom", "type": "uint32" },
        { "name": "padLeft", "type": "uint32" },
        { "name": "strideX", "type": "uint32" },
        { "name": "strideY", "type": "uint32" },
        { "name": "dilationX", "type": "uint32" },
        { "name": "dilationY", "type": "uint32" },
        { "name": "dataLayout", "type": "DataLayout" }
      ],
      //"inputs"对应的值是一个列表,列表内的元素表示该种网络层的输入,表达了对应的含义
      // 这里分别是输入、权重、偏置 
      "inputs": [
        { "name": "input" },
        { "name": "weights" },
        { "name": "biases" }
      ]
    },
    /* <---------卷积层的元描述结束------------> */
    {
      "name": "AdditionLayer",
      "inputs": [
        { "name": "A" },
        { "name": "B" }
      ],
      "outputs": [
        { "name": "C" }
      ]
    },
    // 后面也有其他的层描述
    // ......
]

        按照上面的json中的注释和卷积的例子,我们可以在json中添加更多的不同类型的网络层的元描述。之后,我把编写好的json文件保存为了pzk-metadata.json文件,放置在了model-flatbuffer文件夹下:

$ ls model-flatbuffer
pzk-metadata.json  pzk-schema.fbs

2.3、配置json解析的相关库

        我们目标平台的编程语言采用了C++,因此需要寻找一个能够在C++中解析json文件的库,最好简单依赖库少些。为此,通过互联网搜索,我定位了一个名叫json11的开源工程,其地址如下所示:

json11,一个轻量化的json解析C++库https://github.com/dropbox/json11        我们可以经过如下所示的命令配置到我们的工程中:

$ mkdir 3rdparty
$ cd 3rdparty
$ git clone https://github.com/dropbox/json11.git
$ cp json11_master/json11.hpp ../include
$ mkdir -p ../src
$ cp json11_master/json11.cpp ../src
$ cd ..

三、正式编码

        这一步算是耗时较多的步骤了,主要原因是:需要考虑到接口的通用性和用户友好。一般而言,如果代码量较多,而且层次结构比较清晰的话,会采用一种自顶而下、全局到局部的方式进行代码。但是我个人倾向自底向上、局部到全局的开发方式。因为个人认为,如果从用户的角度来看,搭建一个网络,首先需要构建输入,然后构建网络层;而构建网络层则需要构建属性描述;最后在设置输出。从实例化一种类型的网络层而言,似乎从局部到整体的这种方式更加符合为专ai模编写API的编码方式。

3.1、json规范类编写

        从局部到整体,我们也根据这种方式编写json规范类,既然json文件中整体是列表,列表元素是不同类型的网络层的元描述,那么我们首先构建单独的元描述名为min_meta,一个min_meta就包含了一种类型的网络层的元描述,从min_meta的成员变量就可以看出其是对json一个元描述的重构了:

// one layer describe from json file
class min_meta
{
private:
    /* data */
public:
    min_meta(){};
    min_meta(json11::Json onelayer);
    ~min_meta();
    void print();
    std::string name; //网络类型的名称,比如"Convolution2dLayer"
    std::string category; //属于种类,一般是"Layer"
    /* 属性字典,比如:
    {"padTop":"uint32", "padRight":"uint32"}
    */
    std::map<std::string, std::string> attributes;
    std::vector<std::string> inputs;
    std::vector<std::string> outputs;
    std::string nkey = "name";
    std::string ckey = "category";
    std::string akey = "attributes";
    std::string ikey = "inputs";
    std::string okey = "outputs";
};

        从局部到整体,那么关于json文件,我们又把min_meta当作列表元素,封装成了jsonmeta类,表示C++中对应json规范化文件的类,如下所示:

// for describe the json file to class
class jsonmeta
{
private:
    void _getinfo();
public:
    jsonmeta(){};
    jsonmeta(json11::Json jmeta);
    ~jsonmeta();
    void printinfo();
    bool has_layer(std::string);
    min_meta get_meta(std::string layer);
    void updata(json11::Json jmeta);
    std::vector<min_meta> meta; //元描述列表,包含了所有的不同种类的网络层的元描述
    std::map<std::string, size_t> laycategory;
    std::vector<std::string> layname;
};

       在jsonmeta和flatbuffers之间现在存在着一条鸿沟,我们还无法通过jsonmeta中的min_meta来规范化网络层的实例化,因此我们特意创建了layer_maker这个类,自如其名,主要用于构建一个网络层的,也就是实例化一个网络层,用户可以根据layer_maker的接口设置网络层的属性、设置权重、设置输出信息等,如下所示:

// for build the layer
class layer_maker
{
private:
    /* data */
public:
    layer_maker();
    layer_maker(min_meta layer_meta, uint32_t layerid, std::string layername);
    ~layer_maker();
    bool add_input(uint32_t id, std::string input_name = "");    
    bool add_output(uint32_t id, std::string output_name = "" , bool force_set = true);
    bool add_attr(std::string key, std::vector<uint8_t> buf);
    static DataType string2datatype(std::string a);
    static std::vector<uint32_t> return_id(std::vector<Conn> a);
    min_meta meta_info;
    uint32_t layer_id;
    std::string type;
    std::string name;
    uint8_t input_num = 0;
    uint8_t output_num = 0;
    bool require_attrs = false;
    std::vector<struct Conn> input_id;
    std::vector<struct Conn> output_id;
    struct Attrs attrs;
};

3.2、模型实例化构建器编写

        要说关键性,模型实例化构建器是最为关键的部分了。该构建器的作用是:整合了Json规范化文件的解析和maker_layer这个层构建器;对接了schema.fbs描述的模型协议,提供了可以生成模型、保存模型的功能。

        因此,我的模型实例化构建器命名为PzkM,其主要组成如下所示:

// main class for build pzkmodel
class PzkM
{
private:
    /* data */
public:
    PzkM();
    PzkM(std::string jsonfile);
    ~PzkM();

    void add_info(std::string author="pzk", std::string version="v1.0", std::string model_name="Model");
    void create_time();
    uint32_t layout_len(DataLayout layout);
    std::vector<uint32_t> remark_dims(std::vector<uint32_t> dims, DataLayout layout);
    uint32_t add_input(std::vector<uint32_t> dims, DataLayout layout = DataLayout_NCHW, DataType datatype = DataType_FP32);
    uint32_t add_tensor(std::vector<uint32_t> dims, std::vector<uint8_t> weight, DataLayout layout = DataLayout_NCHW ,TensorType tensor_type = TensorType_CONST , DataType datatype = DataType_FP32);
    bool add_layer(layer_maker layerm);
    bool set_as_output(uint32_t id);
    bool has_tensor(uint32_t id);
    bool has_layer(uint32_t id);
    bool model2file(std::string filepath);
    layer_maker make_empty_layer(std::string layertype, std::string layername = "");
    static uint32_t datatype_len(DataType datatype);
    static json11::Json ReadJson(std::string file);
    static uint32_t shape2size(std::vector<uint32_t> dims);
    jsonmeta meta;
    std::string author;
    std::string version;
    std::string model_name;
    struct tm * target_time;
    uint32_t model_runtime_input_num = 0;
    uint32_t model_runtime_output_num = 0;
    std::vector<uint32_t> model_runtime_input_id;
    std::vector<uint32_t> model_runtime_output_id;
    std::vector<flatbuffers::Offset<Tensor>> tensors;
    std::vector<uint32_t> all_tensor_id;
    std::vector<flatbuffers::Offset<Layer>> layers;
    std::vector<uint32_t> all_layer_id;
    flatbuffers::FlatBufferBuilder builder;
    uint32_t tensor_id = 0;
    uint32_t layer_id = 0;
    
};

        通过这个PzkM类,连接了json和schema,实现了用户模型构建和保存的能力。最终把以上所有的实现和声明都保存在pzk.hpp,放置在include目录下:

$ ls include
json11.hpp  pzk.hpp  pzk-schema_generated.h

四、构建专ai模的第一个模型

4.1、实例编写

        下面例子,构建了一个只包含一层卷积层的网络,所有的步骤和注解都标注在代码中:

#include "pzk.hpp"
#include <iostream>

std::vector<float> rand_weight(uint32_t num=100)
{
    srand(num);
    std::vector<float> weight;
    for (size_t i = 0; i < num; i++)
    {
        weight.push_back( ((rand() % 10) - 4.5f) / 4.5f);
    }
    return weight;
}
template<class T>
std::vector<uint8_t> fp2ubyte(std::vector<T> w1)
{
    std::vector<uint8_t> buf;
    for (size_t i = 0; i < w1.size(); i++)
    {
        T* one = &w1[i];
        uint8_t* charp = reinterpret_cast<uint8_t*>(one); 
        buf.push_back(charp[0]);
        buf.push_back(charp[1]);
        buf.push_back(charp[2]);
        buf.push_back(charp[3]);
    }
    return buf;
    
}


int main(int argc, char **argv) {
    if(argc == 3 && argv[1] == std::string("--json"))
    {
        // 1. 用json文件初始化模型实例化构建器,用来进行规范化
        PzkM smodel(argv[2]);
        //PzkM smodel("/home/pack/custom-model/model-flatbuffer/pzk-metadata.json");
        // 2. 增加模型的附属信息,比如名字和模型版本号等,以及增加模型创建时间
        smodel.add_info("pengzhikang", "v2.1", "holly-model");
        smodel.create_time();
        // 3. 为模型创建一个输入
        std::vector<uint32_t> input_dims = {1,3,416,416};
        uint32_t input_id = smodel.add_input(input_dims);
        // 4. 为模型实例化一个卷积层,加入到模型内部
        // 4.1 获得layer_maker,也就是层实例化构建器
        layer_maker l = smodel.make_empty_layer("\"Convolution2dLayer\"", "conv2d-index-1");
        // 4.2 为该卷积层添加权重
        std::vector<float> org_weight  = rand_weight(10*3*4*4);
        std::vector<uint32_t> wdims;
        wdims.push_back(10);
        wdims.push_back(3);
        wdims.push_back(4);
        wdims.push_back(4);
        uint32_t weight_id = smodel.add_tensor(wdims,
                                            fp2ubyte<float>(org_weight));
        // 4.3 为该卷积层添加偏置
        std::vector<float> org_bias = rand_weight(10);
        uint32_t bias_id = smodel.add_tensor(std::vector<uint32_t>({10}),
                                            fp2ubyte<float>(org_bias));
        // 4.4 为该卷积层添加输出
        uint32_t output_id = smodel.add_tensor(std::vector<uint32_t>({1,10,416/4,416/4}),
                                                std::vector<uint8_t>(), DataLayout_NCHW, TensorType_DYNAMIC);
        // 4.5 为该卷积层添加属性配置信息
        l.add_input(input_id, "\"input\"");
        l.add_input(weight_id, "\"weights\"");
        l.add_input(bias_id, "\"biases\"");
        l.add_output(output_id, "\"conv2d-output\"");
        l.add_attr("\"padTop\"", fp2ubyte<uint32_t>(std::vector<uint32_t>({0})));
        // 4.6 把配置好的卷积层添加到该模型中
        smodel.add_layer(l);
        // 4.7 设置模型的输出信息
        smodel.set_as_output(output_id);
        // 4.8 把模型生成为模型文件
        smodel.model2file("first.PZKM");
        
    }
    return 0;
}

        该例子编写后,保存为create_model_sample.cpp,防置在src目录下。

4.2、实例编译和运行

        编译采用了cmake进行编译,具体的CMakeLists.txt如何编写,可以从后续的整个工程下载链接下载到整个工程,然后自行编译工程。编译命令如下所示:

# 编译命令如下
$ mkdir build
$ cd build
$ cmake ..
$ make -j16

        编译后将在build/release目录中出现名字叫做first_mdel的可执行程序,进行如下的运行命令:

# 运行命令如下
$ cd release
$ ./first_model --json ../../model-flatbuffer/pzk-metadata.json
# 运行后,将打印出如下信息
Result: open ../../model-flatbuffer/pzk-metadata.json success
this model size is 3288
# 同时在该目录下生成名字叫做first.PZKM的模型文件,这就是专ai模型的第一个模型文件了
$ ls
first_model first.PZKM

        其中的first.PZKM就是我们的第一个模型文件了。

4.3、验证模型是否正确

        因为上述的模型是二进制文件,不具备可读性,此时我们可以使用flatc工具把二进制的first.PZKM模型文件重新解释成json文本文件。此时就可以查看模型内部的信息了。使用如下命令:

$ flatc --raw-binary -t model-flatbuffer/pzk-schema.fbs -- build/release/first.PZKM
$ cat first.json

        此时我们将得到如下所示的模型可读信息:

{
  author: "pengzhikang",
  create_time: {
    year: 2021,
    month: 12,
    day: 9,
    hour: 23,
    min: 38,
    sec: 53
  },
  version: "v2.1",
  model_name: "holly-model",
  model_runtime_input_num: 1,
  model_runtime_output_num: 1,
  model_runtime_input_id: [
    0
  ],
  model_runtime_output_id: [
    3
  ],
  all_tensor_num: 4,
  tensor_buffer: [
    {
      name: "model_input_0",
      tesor_type: "DYNAMIC",
      data_type: "FP32",
      shape: {
        dimsize: 4,
        dims: [
          1,
          3,
          416,
          416
        ]
      }
    },
    {
      id: 1,
      name: "tensor_1",
      data_type: "FP32",
      shape: {
        dimsize: 4,
        dims: [
          10,
          3,
          4,
          4
        ]
      },
      weights: {
        ele_bytes: 4,
        ele_num: 480,
        buffer: [...]
      }
    },
    {
      id: 2,
      name: "tensor_2",
      data_type: "FP32",
      shape: {
        dimsize: 4,
        dims: [
          10,
          1,
          1,
          1
        ]
      },
      weights: {
        ele_bytes: 4,
        ele_num: 10,
        buffer: [...]
      }
    },
    {
      id: 3,
      name: "tensor_3",
      tesor_type: "DYNAMIC",
      data_type: "FP32",
      shape: {
        dimsize: 4,
        dims: [
          1,
          10,
          104,
          104
        ]
      }
    }
  ],
  layer_num: 1,
  layer_buffer: [
    {
      name: "conv2d-index-1",
      type: "\"Convolution2dLayer\"",
      input_num: 3,
      input_id: [
        {
          name: "\"input\"",
          necessary: true
        },
        {
          name: "\"weights\"",
          necessary: true,
          tensor_id: 1
        },
        {
          name: "\"biases\"",
          necessary: true,
          tensor_id: 2
        }
      ],
      output_id: [
        {
          name: "\"conv2d-output\"",
          necessary: true,
          tensor_id: 3
        }
      ],
      require_attrs: true,
      attrs: {
        type: "\"Convolution2dLayer\"-Attrs",
        meta_num: 9,
        meta_require_num: 9,
        buffer: [
          {
            key: "\"padTop\"",
            require: true,
            buffer_data: "CHAR",
            buffer: [
              0,
              0,
              0,
              0
            ]
          },
        ]
      }
    }
  ]
}

        上图所示的json被我进行了适当的删减,只为更好的进行展示,从文本文件的描述中,我们验证了我们的专ai模终于派上了用场,我们终于得到了第一个完完全全自定义的模型文件!!

        目前,工程已经上传到了github上,链接如下所示:pengzhikang/Custom-Modelhttps://github.com/pengzhikang/Custom-Model

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值