之前都是使用python训练模型,使用python加载模型预测结果。正好遇到了一个需要使用C++加载模型预测结果的需求,趁这个机会学习一下相关的流程。
对于C++的TensorFlow Api,官方文档介绍只能通过bazel
编译使用。所以先将介绍如何使用bazel
来编译调用TensorFlow模型的C++代码。
一、准备工作
bazel 安装
bazel官方文档写的很清楚,可以根据自己的平台选择安装方法,我是Ubuntu
系统,直接输入下列命令即可:
1 | chmod +x bazel-<version>-installer-linux-x86_64.sh |
可以设置一下环境变量(并不是必须的,因为我执行完上面两个命令就可以了):
1 | export PATH="$PATH:$HOME/bin" |
下载TensorFlow的源代码
因为使用bazel
编译的方式需要TensorFlow的源代码,所以第一步需要下载好源代码:
1 | git clone --recursive https://github.com/tensorflow/tensorflow |
二、准备好模型部分
我们准备一个简单的实现 x * w
的tf代码,不需要训练,只需要能输出结果,能够保存模型即可。
1 | # train.py |
运行上面的代码,可以得到 [[3],[7]]
的输出结果,且在demo_model/
下存好了对应的模型:
1 | demo_model |
接下来,我们写一个简单的python代码调用模型,保证模型没有问题:
1 | import os |
我们可以看到输出了与训练阶段相同的模型输出,证明model调起没什么问题。接下来就是比较困难的C++调用部分。
三、C++ 调用
C++ 代码
首先定义好graph和checkpoint的路径
1 | const string pathToGraph = "yourpath/demo_model/demo.meta"; |
新建session,如果新建没有成功则退出。
1 | auto session = NewSession(SessionOptions()); |
加载存储的graph结构,如果加载不成功则退出。
1 | MetaGraphDef graph_def; |
根据载入的图结构以及之前创建的session对象新建一个session:
1 | status = session->Create(graph_def.graph_def()); |
载入存储的模型参数,通过session->Run的方式重新加载模型参数
1 | Tensor checkpointPathTensor(DT_STRING, TensorShape()); |
构造模型的输入数据,此处演示了一个batch_size为2的二维矩阵的输入。input相当于python版本中的feed_dict。
1 | // input相当于python版本中的feed_dict。 |
调用模型,获取输出结果。
1 | // 结果是Tensor的向量 |
其实大部分代码都是参考网友实现的,官方的API真的很晦涩难以阅读。目前来看,想用c++构造输入输出都很不灵活。整个c++文件如下:
1 |
|
编译
可以在tensorflow/tensorflow
文件夹下新建demo文件夹,将上面的C++代码放入该文件夹。同时新建一个BUILD文件,里面内容如下,这个bazel
的类似于makefile的编译文件,主要定义目标的名字,源文件是什么,以及依赖。具体可以参考教材Introduction to Bazel: Building a C++ Project
1 | load("//tensorflow:tensorflow.bzl", "tf_cc_binary") |
回到仓库的根目录,执行下面的编译语句。//tensorflow/demo
是BUILD文件的位置,demo
表示目标文件的名字。
1 | bazel build //tensorflow/demo:demo |
第一次编译需要很久,之后就很快了。在bazel_bin/tensorflow/demo
文件夹下会有编译好的demo
可执行文件。在该目录下执行命令
1 | ./demo |
即可得到模型计算出的答案[[3],[7]]
。
使用GPU运行
首先,如果希望编译出的代码在运行时可以调用GPU运行,那么必须在重新配置TensorFlow(在仓库根目录下,运行命令./configure
),需要enable cuda,如下所示:
1 | Do you wish to build TensorFlow with CUDA support? [y/N]: y |
之后需要按照命令行提示填写cuda版本和路径,cudnn的版本和路径。配置完毕后,在使用bazel
编译代码时,加上--config=cuda
的参数,编译命令如下:
1 | bazel build -c opt --config=cuda //tensorflow/demo:demo |
再运行demo
,则发现预测过程是在GPU上计算了。
四、指定GPU运行
上面阐述的方法主要是对metaGraphDef进行加载,然后再载入checkpoint的参数。这种方法有一个局限性,即无法在同一个程序中指定多个GPU。
一般来说,我们可以通过设置"CUDA_VISIBLE_DEVICES"
环境变量的方式来指定模型需要使用的GPU,但是在C++部署的生产环境中,如果需要使用多个模型进行集成,可以并行使用多个GPU进行预测是较为理想的方案。所以,此时不能再通过设置"CUDA_VISIBLE_DEVICES"
环境变量的方法来指定GPU了。
一般来说,C++中指定GPU有两种方法,一种是通过tensorflow::SessionOptions()
进行设置,代码如下:
1 | auto options = tensorflow::SessionOptions(); |
这种方法的问题是这个设置目前是进程级别的设置,所以在单个进程中无法对多个模型指定不同的设备。相关可以参考https://github.com/tensorflow/tensorflow/issues/18861。
第二种方法是通过遍历图中节点,将图中的节点手动移动到指定的device中。实际上,TensorFlow也提供了相关的函数,如下所示
1 | inline void SetDefaultDevice(const string& device, GraphDef* graph_def) { |
很遗憾,通过metaGraphDef获取的GraphDef是const的,所以无法对其进行修改。
最终,我们不读取metaGraphDef的方式,采用读取GraphDef的方式加载模型,而GraphDef对应的是pb格式的模型。
metaGraphDef和GraphDef的区别
待总结。先贴个博客Tensorflow框架实现中的“三”种图
如何存储pb格式模型
存储pb格式模型有两种方法:
- 使用
convert_variables_to_constants
,但是这个接口未来会被弃用
1 | from tensorflow.python.framework.graph_util import convert_variables_to_constants |
- 使用
freeze_graph.py
使用 freeze_graph.py
可以将没有保存参数的GraphDef和离线存储的参数整合到一起,形成可以直接使用的模型。
1 | saver.save(sess, "models/test") // 保存checkpoint文件 |
通过上面的代码保存了pb文件和checkpoint文件,然后通过 freeze_graph.py
则可以固化模型。
1 | python freeze_graph.py --input_graph "graph.pb" --input_checkpoint "test" --output_graph "graph_scrpit.pb" --output_node_names "output/pred_y" --input_binary=true |
C++代码
1 |
|
五、总结
至此,使用bazel
编写调用TensorFlow离线模型的C++基本功能已经实现了。但是官方文档不是很全面,实现的方法还不灵活,如果遇到了其他的应用场景很可能代码不能正常工作了。所以需要找到比较好的文档,更加深入了解TensorFlow的C++ API。
另外,官方文档提到C++ API的调用只能通过bazel
编译的方式,而C API可以通过先将TensorFlow编译为动态链接库,再使用其他编译工具链接到自己代码的方法使用,可以一试。