## C-API CPU 单线程预测示例 这篇文档通过一个最简单的例子:手写数字识别,来介绍 CPU 下单线程使用 PaddlePaddle C-API 开发预测服务,完整代码见[此目录](https://github.com/PaddlePaddle/Paddle/blob/develop/paddle/capi/examples/model_inference/dense/)。 ### 使用流程 使用 C-API 分为:准备预测模型和预测程序开发两部分。 - 准备预测模型 1. 将神经网络模型结构进行序列化。 - 调用C-API预测时,需要提供序列化之后的网络结构和训练好的模型参数文件。 1. 将PaddlePaddle训练出的模型参数文件(多个)合并成一个文件。 - 神经网络模型结构和训练好的模型将被序列化合并入一个文件。 - 预测时只需加载这一个文件,便于发布。 - **注意**:以上两种方式只需选择其一即可。 - 调用 PaddlePaddle C-API 开发预测序 1. 初始化PaddlePaddle运行环境。 1. 加载模型。 1. 创建神经网络的输入,组织输入数据。 1. 进行前向计算,获得计算结果。 1. 清理。 本文档以手写数字识别任务为例,介绍如何使用 C-API 进行预测,完整代码请查看[此目录](https://github.com/PaddlePaddle/Paddle/tree/develop/paddle/capi/examples/model_inference/dense)。 ### 准备预测模型 通过在终端执行`python mnist_v2.py` 运行[目录](https://github.com/PaddlePaddle/Paddle/tree/develop/paddle/capi/examples/model_inference/dense)下的 `mnist_v2.py` s可以使用 PaddlePaddle 内置的 [MNIST 数据集](http://yann.lecun.com/exdb/mnist/)进行训练。脚本中的模型定义了一个简单的含有[两个隐层的全连接网络](https://github.com/PaddlePaddle/book/blob/develop/02.recognize_digits/README.cn.md#softmax回归softmax-regression),网络接受一幅图片作为输入,将图片分类到 0 ~ 9 类别标签之一。训练好的模型默认保存在当前运行目录下的`models`目录中。下面,我们将调用 C-API 加载训练好的模型进行预测。 1. 序列化神经网络模型配置 PaddlePaddle 使用 protobuf 来传输网络配置文件中定义的网络结构和相关参数,在使用 C-API 进行预测时,也需将网络结构使用 protobuf 进行序列化,写入文件中。 调用`paddle.utils.dump_v2_config`中的`dump_v2_config`函数能够将使用 PaddlePaddle V2 API 定义的神经网络结构 dump 到指定文件中。示例代码如下: ```python from paddle.utils.dump_v2_config import dump_v2_config from mnist_v2 import network predict = network(is_infer=True) dump_v2_config(predict, "trainer_config.bin", True) ``` 对本例,或运行 `python mnist_v2.py --task dump_config`,会对示例中的网络结构进行序列化,并将结果写入当前目录下的`trainer_config.bin`文件中。 当选择使用这种方式调用 C-API 时,如果神经网络有多个可学习参数,请将它们全部放在同一文件夹内,C-API会从指定的目录寻找并加载训练好的模型。 2. 合并模型文件(可选) 一些情况下为了便于发布,希望能够将序列化后的神经网络结构和训练好的模型参数打包进一个文件,这时可以使用`paddle.utils.merge_model`中的`merge_v2_model`接口对神经网络结构和训练好的参数进行序列化,将序列化结果写入一个文件内,调用C-API时直接只需加载这一个文件。 代码示例如下: ```python from paddle.utils.merge_model import merge_v2_modelss from mnist_v2 import network net = network(is_infer=True) param_file = "models/params_pass_4.tar" output_file = "output.paddle.model" merge_v2_model(net, param_file, output_file) ``` 对本例,或者直接运行 `python merge_v2_model.py`,序列化结果将会写入当前目录下的`output.paddle.model`文件中,该文件在调用C-API时,可被直接加载。 #### 注意事项 1. C-API 需要序列化之后神经网络结构,在调用`dump_v2_config`时,参数`binary`必须指定为`True`。 1. **预测使用的网络结构往往不同于训练**,通常需要去掉网络中的:(1)类别标签层;(2)损失函数层;(3)`evaluator`等,只留下核心计算层,请注意是否需要修改网络结构。 1. 预测时,可以获取网络中定义的任意多个(大于等于一个)层前向计算的结果,需要哪些层的计算结果作为输出,就将这些层加入一个Python list中,作为调用`dump_v2_config`的第一个参数。 ### 编写预测代码 #### step 1. 初始化PaddlePaddle运行环境 使用C-API第一步需首先调用[`paddle_init`](https://github.com/PaddlePaddle/Paddle/blob/develop/paddle/capi/main.h#L27) 初始化PaddlePaddle运行环境。接口接受两个参数:参数的个数和参数。 下面的代码片段在初始化PaddlePaddle运行环境时指定不使用GPU: ```c // Initalize the PaddlePaddle runtime environment. char* argv[] = {"--use_gpu=False"}; CHECK(paddle_init(1, (char**)argv)); ``` 下面的代码片段在初始化PaddlePaddle运行环境时指定了两个参数:不使用GPU和[使用MKLDNN](https://github.com/PaddlePaddle/Paddle/blob/develop/doc/design/mkl/mkldnn.md): ```c char* argv[] = {"--use_gpu=False", "--use_mkldnn=True"}; CHECK(paddle_init(2, (char**)argv)); ``` #### step2. 加载模型 这里介绍C-API使用中的一个重要概念:Gradient Machine。概念上,在 PaddlePaddle 内部一个GradientMachine类的对象管理着一组计算层(PaddlePaddle Layers)来完成前向和反向计算,并处理与之相关的所有细节。在调用C-API预测时,只需进行前向计算而无需调用反向计算。这篇文档的之后部分我们会使用`gradient machine`来特指调用PaddlePaddle C-API创建的GradientMachine类的对象。 每一个 `gradient machine` 都会管理维护一份训练好的模型,下面是两种最常用的模型加载方式: 1. 从磁盘加载:这时`gradient machine`会独立拥有一份训练好的模型; 1. 共享自其它`gradient machine`的模型:这种情况多出现在使用多线程预测时,通过多个线程共享同一个模型来减少内存开销。可参考[此示例](https://github.com/PaddlePaddle/Paddle/blob/develop/paddle/capi/examples/model_inference/multi_thread/main.c)。 下面的代码片段创建 `gradient machine`,并从指定路径加载训练好的模型。 ```c // Read the binary configuration file generated by `convert_protobin.sh` long size; void* buf = read_config(CONFIG_BIN, &size); // Create the gradient machine for inference. paddle_gradient_machine machine; CHECK(paddle_gradient_machine_create_for_inference(&machine, buf, (int)size)); // Load the trained model. Modify the parameter MODEL_PATH to set the correct // path of the trained model. CHECK(paddle_gradient_machine_load_parameter_from_disk(machine, MODEL_PATH)); ``` ##### 注意事项 1. 以上代码片段使用“仅序列化神经网络结构”的方式加载模型,需要同时指定模型参数存储的路径。 - 使用PaddlePaddle V2 API训练,模型中所有可学习参数会被存为一个压缩文件,需要手动进行解压,将它们放在同一目录中,C-API不会直接加载 V2 API 存储的压缩文件。 1. 如果使用`merge model`方式将神经网络结构和训练好的参数序列化到一个文件,请参考此[示例](https://github.com/PaddlePaddle/Mobile/blob/develop/Demo/linux/paddle_image_recognizer.cpp#L59)。 1. 加载模型有多种方式,也可以在程序运行过程中再加载另外一个模型。 #### step 2. 创建神经网络输入,组织输入数据 基本使用概念: - 在PaddlePaddle内部,神经网络中一个计算层的输入输出被组织为一个 `Argument` 结构体,如果神经网络有多个输入或者多个输出,每一个输入/输出都会对应有自己的`Argument`。 - `Argument` 并不真正“存储”数据,而是将输入/输出数据有机地组织在一起。 - 在`Argument`内部由:1. `Matrix`(二维矩阵,存储浮点类型输入/输出);2. `IVector`(一维数组,**仅用于存储整型值**,多用于自然语言处理任务)来实际存储数据。 *注:本文档使用的示例任务手写数字识别不涉及一维整型数组作为输入,因此,本文档仅讨论二维稠密矩阵作为输入的情形。更多输入数据格式请参考输入/输出数据一节的内容。* 这篇文档的之后部分会使用`argument`来**特指** PaddlePaddle C-API中神经网络的一个输入/输出,使用`paddle_matrix`**特指**`argument`中用于存储数据的`Matrix`类的对象。 于是,在组织神经网络输入,获取输出时,需要思考完成以下工作: 1. 为每一个输入/输出创建`argument`; 1. 为每一个`argument`创建`paddle_matrix`来存储数据; 与输入不同的是,输出`argument`的`paddle_matrix`变量并不需在使用C-API时为之分配存储空间。PaddlePaddle内部,神经网络进行前向计算时会自己分配/管理每个计算层的存储空间;这些细节C-API会代为处理,只需在概念上理解,并按照约定调用相关的 C-API 接口即可。 下面是示例代码片段。在这段代码中,生成了一条随机输入数据作为测试样本。 ```c // Inputs and outputs of the network are organized as paddle_arguments object // in C-API. In the comments below, "argument" specifically means one input of // the neural network in PaddlePaddle C-API. paddle_arguments in_args = paddle_arguments_create_none(); // There is only one data layer in this demo MNIST network, invoke this // function to create one argument. CHECK(paddle_arguments_resize(in_args, 1)); // Each argument needs one matrix or one ivector (integer vector, for sparse // index input, usually used in NLP task) to holds the real input data. // In the comments below, "matrix" specifically means the object needed by // argument to hold the data. Here we create the matrix for the above created // agument to store the testing samples. paddle_matrix mat = paddle_matrix_create(/* height = batch size */ 1, /* width = dimensionality of the data layer */ 784, /* whether to use GPU */ false); paddle_real* array; // Get the pointer pointing to the start address of the first row of the // created matrix. CHECK(paddle_matrix_get_row(mat, 0, &array)); // Fill the matrix with a randomly generated test sample. srand(time(0)); for (int i = 0; i < 784; ++i) { array[i] = rand() / ((float)RAND_MAX); } // Assign the matrix to the argument. CHECK(paddle_arguments_set_value(in_args, 0, mat)); ``` #### step 3. 前向计算 完成上述准备之后,通过调用 `paddle_gradient_machine_forward` 接口完成神经网络的前向计算。 示例代码片段如下: ```c // Create the output argument. paddle_arguments out_args = paddle_arguments_create_none(); // Invoke the forward computation. CHECK(paddle_gradient_machine_forward(machine, in_args, out_args, s/* is train taks or not */ false)); // Create the matrix to hold the forward result of the neural network. paddle_matrix prob = paddle_matrix_create_none(); // Access the matrix of the output argument, the predicted result is stored in // which. CHECK(paddle_arguments_get_value(out_args, 0, prob)); uint64_t height; uint64_t width; CHECK(paddle_matrix_get_shape(prob, &height, &width)); CHECK(paddle_matrix_get_row(prob, 0, &array)); printf("Prob: \n"); for (int i = 0; i < height * width; ++i) { printf("%.4f ", array[i]); if ((i + 1) % width == 0) { printf("\n"); } } printf("\n"); ``` #### step 4. 清理 结束预测之后,对使用的中间变量和资源进行清理和释放: ```c // The cleaning up. CHECK(paddle_matrix_destroy(prob)); CHECK(paddle_arguments_destroy(out_args)); CHECK(paddle_matrix_destroy(mat)); CHECK(paddle_arguments_destroy(in_args)); CHECK(paddle_gradient_machine_destroy(machine)); ```