美文网首页
Windows环境下使用CPU版tensorflow 1.15

Windows环境下使用CPU版tensorflow 1.15

作者: 水击长空 | 来源:发表于2020-07-07 18:19 被阅读0次

    写在前面:

    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版 - 哔哩哔哩

    相关文章

      网友评论

          本文标题:Windows环境下使用CPU版tensorflow 1.15

          本文链接:https://www.haomeiwen.com/subject/gzszqktx.html