写在前面:
1、Linux环境下编译更简单,环境也更好配,但Linux环境下编译出的是.so格式的动态库文件,无法在Windows下使用;
2、Tensorflow C++ API在调用pb模型时,对生成pb模型的tensorflow版本很敏感,笔者使用tf 1.12的api调用tf1.15环境下的pb模型直接提示mismatch;
3、虽然笔者踩了很多坑,但也不能保证按本文一定能成功,但不放弃总能成的.......吧。
一、前期工作
这一段没有什么干货,赶时间的读者可以直接拉滑动条到第二章节~~~
接到在C++里调用tensorflow的任务后,笔者隐约觉得不太好弄,便咨询了一波同行同业,看看有没有人有这方面的经验:
在研究所的A:为啥用C++?为了部署服务加速嘛?那用GPU+TensorRT不香嘛?(羡慕这样的硬件条件)
在智能制造的B:这个以前Cmake搞过啊,不记得了,现在不弄这个了(果然搞硬件的人这方面经验丰富一些)
在互联网的C:照着官网的流程来吧,翻个墙,加加班,也就花个几天时间(现在想想,这段话的重点其实是翻个墙)
一波问下来,既然没什么新路子,那就还是从官网开始吧。从源代码构建 | TensorFlow
官网的流程一如既往地简约,但实际过程真的是“简约而不简单”。几天下来,笔者翻阅了数十份记录tf不同版本的编译过程的博客,总结下来主要是4代:
使用Cmake编译的最早一代,以1.8版本为代表(实在太老了,17年开始笔者在python上就已经是1.12版了),
参考tensorflow-windows-build-script的1.11到1.13,其中1.12貌似是用的最多的(笔者尝试了别人编好的1.12,结果遇到了模型不匹配的问题),
记录比较少的1.14(笔者在1.15上的编译主要参考这个版本),
以及tf2.0之后的(这个版本笔者也试了,结果调用的时候报一歌关于absl的无法解析的外部符号的错误,始终没能解决)
在这个过程中,笔者共编译了Ubuntu环境下的1.14、Windows环境下的1.14、1.15以及2.1共4份头文件,都是CPU版本,本文主要以1.15为主。
二、编译环境搭建
1、Msys2
直接到官网下载MSYS2,全程按默认安装,安装完成以后,将目录C:\msys64和C:\msys64\usr\bin 加入到系统环境变量的path中。
再打开cmd.exe,输入命令
pacman -Syuu patch
2、bazel
bazel是编译中最重要的部分,也是最作妖的。对于tensorflow、python、bazel的版本问题,请参考官网上经过测试的构建配置(是的,官网没有1.15的推荐配置,咱要编一个非主流版本)。需要提醒一点的是,bazel在编译过程中会用到Visual Studio,但0.26.0以后的版本才可以使用Visual Studio 2019。
笔者的版本配置如下:
直接从githubReleases · bazelbuild/bazel · GitHub下载对应版本的exe文件,如果网速不佳,可以使用参考2中的方法,右键需要下载的文件,复制链接地址,然后去这个网站下载。
下载完成后,把下好的文件改名为bazel.exe,放到C:\msys64目录下。
然后新建系统环境变量:BAZEL_SH,BAZEL_VC ,BAZEL_VS,三个变量的值分别为(编译tf1.15为例):
C:\msys64\usr\bin\bash.exe
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
3、protoc
对应的版本如上表所示,也是直接从githubReleases · protocolbuffers/protobuf · GitHub下载对应版本的文件,解压后将将bin目录加入系统环境变量的path中,再打开cmd.exe,输入命令 protoc,成功返回便为安装成功。
这里再提一下笔者是怎么看看protoc版本的。因为这篇文章讲的是在C++下调用python环境里训练得到的pb模型,所以默认大家都是有装python版的tensorflow1.15的。找到虚拟环境中的tensorflow_core文件夹,一般是在\Anaconda3\envs\虚拟环境的名称\Lib\site-packages\下面,进入到tensorflow_core\include\google\protobuf中,用记事本打开port_def.inc,ctrl+f寻找PROTOBUF_VERSION,会有一串7位的数字,比如3008000就表示版本号是3.8。
4、tensorflow
GitHub - tensorflow/tensorflow: An Open Source Machine Learning Framework for Everyone注意切换branch,下载好后解压,得到tensorflow-r1.15文件夹。
三、bazel编译
接下来就是重头戏了。
首先,在cmd里进入装有python的虚拟环境,然后cd到tensorflow-r1.15目录。
运行
python configure.py
开始配置,这一段也可以参考从源代码构建 | TensorFlow。第一个问题是问python地址,可以直接回车,系统会提供默认的地址,再回车就可以了。后面的问题一律填n,如果需要使用GPU,可以在cuda那一项之后选y(笔者当时为了避免节外生枝,没有尝试,以后可能会试一下)。
到这里就可以开始编译了,命令是
bazel build --config=opt //tensorflow:tensorflow_cc.dll
然后就是等待了,如果在下载依赖包的时候卡住报错,那就退出编译后接着运行上面那句命令,推荐编译的时候翻个墙,下载过程会顺畅很多。运气好的话,等40分钟左右就能出结果了。
这一部分里我没有遇到参考1中提到的无法解析的外部符号的问题。之后在VS中调用头文件时遇到了这类问题,重新编译时修改了tensorflow-r1.15\tensorflow目录下的tf_exported_symbols_msvc.lds,但发现这个文件里的内容对编译的成功与否没什么影响,和tensorflow-r1.15\tensorflow\tools\def_file_filter目录下的def_file_filter.py.tpl文件关系比较大,这应该是和版本有关,具体的下面会提到。
编译完成后,cmd里会打印一条
Build completed successfully
编译好的头文件在tensorflow-r1.15\bazel-bin\tensorflow目录下,分别是tensorflow_cc.dll和tensorflow_cc.dll.if.lib。
四、VS项目环境配置
新建一个文件夹,笔者按版本号命名为libtensorflow-1.15(笔者的文件夹建在D盘下),在文件夹下再新建3个文件夹:
bin:里面放tensorflow_cc.dll;
lib:将tensorflow_cc.dll.if.lib重命名为tensorflow_cc.lib放在里面;
include:几篇参考资料里这个文件夹下的东西都是从不同的地方复制来的,笔者找到一个偷懒的办法,tensorflow文件夹和third_party文件夹使用编译目录tensorflow-r1.15下的那两个,然后将Anaconda3\envs\虚拟环境的名称\Lib\site-packages\tensorflow_core\include下的absl、Eigen、external、google以及unsupported五个文件夹复制过来,最后include文件夹的内容如下图所示:
其实Anaconda3\envs\虚拟环境的名称\Lib\site-packages\tensorflow_core\include下本身也包含一个tensorflow_core和third_party,初步对比下来,源码文件夹tensorflow-r1.15下的tensorflow和third_party里包含的东西更多(如下图,以third_party为例,笔者的虚拟环境的名字为tensorflow)。为了避免导入包含库的时候找不到XX文件,本着多多益善的原则,笔者目前使用内容较多的那两个。但讲道理虚拟环境下的那两个应该也足够,对库文件夹大小敏感的读者可以试一下。
接下来在Visual Studio中打开需要调用pb文件的项目,进入项目——属性,进入属性页后,配置选择Release,平台选择x64,然后点击左侧VC++目录,在包含目录中加入libtensorflow-1.15\include,在库目录中加入libtensorflow-1.15\lib,如下图:
接着点击左侧的链接器——输入,在附加依赖项中加入tensorflow_cc.lib。
此外,还需要将tensorflow_cc.dll和tensorflow_cc.lib复制一份放到项目所在目录下的x64/Release文件夹下,如果还没有x64/Release目录那就生成解决方案后再复制一份放过去。
PS:编译生成的头文件目前只能在Release下使用,在Debug下相比Release会产生很多无法解析的外部符号的错误。虽说接下来会介绍处理这种错误的方法,但为了尽快用起来,笔者就优先编译Release版的了。此外由于接下来不可避免的需要在Release下调试代码,所以在这里可以接着点击属性页左侧的C/C++——优化,然后将右侧优化的值改为已禁用(/Od)。
在Cpp文件头部引入tensorflow
#include"tensorflow/core/public/session.h"
#include"tensorflow/core/platform/env.h"
点击Visual Studio界面最上方的生成——重新生成解决方案,然后就。。。面对疾风吧,错误列表里的错误应该在几十到几百个不等。
1、无法打开xxx.pb.h文件
错误中的一大类是无法打开xxx.pb.h文件,主要是在libtensorflow-1.15\include\tensorflow\core\framework或是D:\libtensorflow-1.15\include\tensorflow\core\protobuf下的,这时候前文提到过的protoc就派上用场了。为了能一次解决问题,务必保证版本匹配。
打开一个cmd.exe,cd到libtensorflow-1.15\include目录下,对照着Visual Studio的错误列表,比如无法打开libtensorflow-1.15\include\tensorflow\core\framework\tensor.pb.h,那就运行以下命令:
protoc --cpp_out=./ ./tensorflow/core/framework/tensor.proto
把所有无法打开的文件都按这个方法生成一遍,然后重新生成解决方案,如果还有这类错误,继续运行上述命令,直到生成的解决方案里没有这一类错误。
错误列表里如果报类似error PROTOBUF_DEPRECATED was previously defined的错误,那就是protoc版本不匹配,需要使用正确版本的protoc将各种pb.h重新生成一遍。
2、 “(”:“::”右边的非法标记、意外的类型“unknown-type”、语法错误:“)” 、语法错误: 缺少“;”(在“{”的前面)
这四个错误乍一看也不知道该如何解决,好在定位到出错的源码位置后,发现正是在参考中许多人都提到过的max、min问题,主流的解决方案就是简单粗暴地加括号,比如
std::numeric_limits::max()
改为
(std::numeric_limits::max)()
笔者还遇到了std::max()也报错,也是直接改成(std::max)()。
此外,笔者发现在Cpp文件的头部加入
#define NOMINMAX
貌似也可以解决这类问题。
3、无法解析的外部符号
之前提到了,在一些其他版本的编译经验中使用tf_exported_symbols_msvc.lds来解决这个问题,但貌似都是在1.11到1.13版本的编译中。
笔者使用的是参考2中的方法,在源码文件夹tensorflow-r1.15\tensorflow\tools\def_file_filter\def_file_filter.py.tpl文件中,定位到Header for the def file这行,在下面加一行def_fp.write("\t 无法解析的符号\n"),如图所示
其中前3行def_fp.write是文件中本来就有的,后两行是笔者加入的。
和前两个错误相比,这类错误相对麻烦一些,因为在修改完def_file_filter.py.tpl文件以后,需要重新走一遍第三章节的操作,不过由于只需要编译修改的部分,耗时会减少很多。编译完成后记得用新的tensorflow_cc.dll和tensorflow_cc.lib替换libtensorflow-1.15里面bin和lib文件夹下的内容,include文件夹不用调整。
在编译2.1版本的时候,有一个错误是:
无法解析的外部符号 "public: __cdecl tensorflow::TensorShapeBase::TensorShapeBase(class absl::lts_2020_02_25::Span<__int64 const >)" (??0?$TensorShapeBase@VTensorShape@tensorflow@@@tensorflow@@QEAA@V?$Span@$$CB_J@lts_2020_02_25@absl@@@Z)
使用上述方法加入def_file_filter.py.tpl文件再进行编译的话,会在编译时报错。google下来找到一个相关的解决方案No C++ symbols exported after built libtensorflow_cc with bazel on windows · Issue #23542 · tensorflow/tensorflow · GitHub,但依旧没能解决。如果诸位中有人解决了这个问题,也请不吝赐教。
至此,生成错误为0的解决方案。
五、C++ API调用pb模型
笔者调用了一个分类任务的pb模型,代码如下:
#include
#define NOMINMAX
#include
#include"tensorflow/core/public/session.h"
#include"tensorflow/core/platform/env.h"
using namespace std;
using namespace tensorflow;
using namespace cv;
int main()
{
const string model_path = "D:\\code\\yinbao_face\\live.pb";
const string image_path = "0.jpg";
Mat img = imread(image_path);
cvtColor(img, img, COLOR_BGR2RGB);
resize(img, img, Size(112, 112), 0, 0, INTER_NEAREST);
int height = img.rows;
int width = img.cols;
int depth = img.channels();
// 图像预处理
img = (img - 0) / 255.0;
img.convertTo(img, CV_32F);
// 取图像数据,赋给tensorflow支持的Tensor变量中
const float* source_data = (float*)img.data;
Tensor input_tensor(DT_FLOAT, TensorShape({ 1, height, width, 3 }));
auto input_tensor_mapped = input_tensor.tensor();
for (int i = 0; i < height; i++) {
const float* source_row = source_data + (i * width * depth);
for (int j = 0; j < width; j++) {
const float* source_pixel = source_row + (j * depth);
for (int c = 0; c < depth; c++) {
const float* source_value = source_pixel + c;
input_tensor_mapped(0, i, j, c) = *source_value;
//printf("%d");
}
}
}
Session* session;
Status status = NewSession(SessionOptions(), &session);
if (!status.ok()) {
cerr << status.ToString() << endl;
return -1;
}
else {
cout << "Session created successfully" << endl;
}
GraphDef graph_def;
Status status_load = ReadBinaryProto(Env::Default(), model_path, &graph_def);
if (!status_load.ok()) {
cerr << status_load.ToString() << endl;
return -1;
}
else {
cout << "Load graph protobuf successfully" << endl;
}
// 将graph加载到session
Status status_create = session->Create(graph_def);
if (!status_create.ok()) {
cerr << status_create.ToString() << endl;
return -1;
}
else {
cout << "Add graph to session successfully" << endl;
}
cout << input_tensor.DebugString() << endl; //打印输入
vector> inputs = {
{ "input_1:0", input_tensor }, //input_1:0为输入节点名
};
// 输出outputs
vector outputs;
vector output_nodes;
output_nodes.push_back("output_1:0"); //输出有多个节点的话就继续push_back
double start = clock();
// 运行会话,最终结果保存在outputs中
Status status_run = session->Run({inputs}, {output_nodes}, {}, &outputs);
Tensor boxes = move(outputs.at(0));
cout << boxes.DebugString() << endl; //打印输出
double end = clock();
cout << "time = " << (end - start) << "\n";
if (!status_run.ok()) {
cerr << status_run.ToString() << endl;
return -1;
}
else {
//cout << "Run session successfully" << endl;
}
}
六、一些操作过程中的建议
1、确定所要编译的tensorflow版本
如笔者在第一部分提到的,网上记录编译的博客可以分为4代,目前来看1.14、1.15以及2.0以后的版本编译过程大致相同,可能遇到的错误也类似。确定版本之后就可以少看一些其他版本的博客,节省时间。
2、科学上网用google
笔者在这个过程中使用了baidu、bing和google三家的搜索引擎,baidu在遇到一些常见问题时比较有用,因为已经有大批的国内程序员踩过坑,CSDN之类的博客可以搜到很多。但遇到具体的英文报错,google往往能得到更好的结果,比如笔者遇到的关于absl的无法解析的外部符号,baidu下来啥也没有,google第一条就找到了github上的issue。
3、不要轻易放弃
笔者从编译头文件到成功调用整整花了5天时间,其中有一些路绕来绕去走了好几遍。
一开始觉得Linux环境下容易编译,略踩几个坑编好之后发现无法在Windows下调用。但笔者用的Linux没有图形界面,无法调试代码,所以只好在Windows下重新开始。
Windowsx环境下第一次选择编译1.14版本没有成功,想要放弃自己编译直接参考1中编好的头文件。在Visual Studio中搞到错误为0后,调用第一个模型提示有op不支持。。。换另一个简单的模型。op都支持了,又提示模型版本不匹配。
考虑到官网推荐的编译成功的版本中没有1.15,就想索性一步到位上2.0的吧。有了前面的编译和调用经验,这一次顺利很多。但最后一通操作后倒在了那个无法解析的外部符号。
无奈最后回到1.15版本,参考1.14和2.1的编译过程,成功上岸。
最后一句: