Windows环境下使用CPU版tensorflow 1.15 C++ API运行pb模型

写在前面:

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。

笔者的版本配置如下:

image

直接从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文件夹的内容如下图所示:

image

其实Anaconda3\envs\虚拟环境的名称\Lib\site-packages\tensorflow_core\include下本身也包含一个tensorflow_core和third_party,初步对比下来,源码文件夹tensorflow-r1.15下的tensorflow和third_party里包含的东西更多(如下图,以third_party为例,笔者的虚拟环境的名字为tensorflow)。为了避免导入包含库的时候找不到XX文件,本着多多益善的原则,笔者目前使用内容较多的那两个。但讲道理虚拟环境下的那两个应该也足够,对库文件夹大小敏感的读者可以试一下。

image
image

接下来在Visual Studio中打开需要调用pb文件的项目,进入项目——属性,进入属性页后,配置选择Release,平台选择x64,然后点击左侧VC++目录,在包含目录中加入libtensorflow-1.15\include,在库目录中加入libtensorflow-1.15\lib,如下图:

image

接着点击左侧的链接器——输入,在附加依赖项中加入tensorflow_cc.lib。

image

此外,还需要将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<difference_type>::max()

改为

(std::numeric_limits<difference_type>::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"),如图所示

image

其中前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<class tensorflow::TensorShape>::TensorShapeBase<class tensorflow::TensorShape>(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 <iostream>

#define NOMINMAX
#include<opencv2/opencv.hpp>
#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<float, 4>();                                                                                      

    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<pair<string, Tensor>> inputs = {
        { "input_1:0", input_tensor },  //input_1:0为输入节点名
    };
    
        // 输出outputs
    vector<Tensor> outputs;
    vector<string> 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的编译过程,成功上岸。

最后一句:
\Large{\color{#A52A2A}{\mathbf{困难是打不倒我们的,奥利给! —张含韵}}}

七、参考

1、windows+bazel+tensorflow-v1.12.0(GPU)编译生成dll与lib

2、WIN10+CUDA10.0+CUDNN7.6.5+Tensorflow1.14 编译及C++调用

3、win10+vs2019+bazel+编译tensorflow2.1-CPU-only版 - 哔哩哔哩

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容