如果读者没有阅读过前两篇,或者想要再次回顾,以下是对应的链接:
你或许也想拥有专属于自己的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