SDL中文论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 4265|回复: 0
打印 上一主题 下一主题

[Discuss] 用Rose构建需要TensorFlow的跨平台app

[复制链接]

187

主题

346

帖子

2450

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2450
跳转到指定楼层
楼主
发表于 2018-2-13 09:44:41 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
Rose已使用TensorFlow Lite,留此贴只是为以后的可能做参考

在产品中使用TensorFlow大致分为两种,第一种部署在服务器,第二种部署在终端。以识别车牌为例,第一种方案时,终端抓拍车牌图像,把图像上传到服务器请求识别,服务器把识别结果反馈给终端。第二种则是终端抓拍车牌图像,直接在本地“运算”出识别结果。Rose是个用于编写app的跨平台工具链,内置TensorFlow是要实现第二种方案,即在终端上完成“全部”识别过程。这里“全部”要加引号,毕竟移动app碍于处理能力等限制,基本不会去执行像训练模型这种任务。

使用神经网络(TensorFlow)解决问题可分为4个步骤。1)提取问题中实体的特征向量作为神经网络输入。2)定义神经网络的结构,并定义如何从神经网络的输入得到输出。3)通过训练数据来调整神经网络中参数的取值,即训练神经网络。4)使用训练好的神经网络(模型)来预测末知数据。移动app涉及到的是步骤1和4,即加载已经训练好的模型,用它来预测末知数据,不涉及构造和训练网络。具体到编程,移动app要做的分为三个任务。1)加载某个已经训练好的模型。2)按模型的输入要求,根据当前问题生成输入参数。3)基于模型创建会话,运行会话,根据模型对输出的定义得出当前问题的识别结果。

裁剪TensorFlow包
  TensorFlow源码中一个很大部分是Op(Operation)相关代码,TensorFlow支持自定义Op,可以这么说,Op代码没有尽头。即使抛开用户自定义,官方定义的Op也是个巨大数字,像v1.2.1,只是kernels目录下的*.cc,官方默认要编译的就达到216个!全编译这些Op,加上源码树中其它200来个cc,如果生成的是可调试静态库,那至少2G以上,即使是不支持调试的Release版,也要100M上下。用的是Visual Studio时,由于要编译结果实在太大,不论用的是2015+update3或2017,就没法编译成功Debug版。
  官方已意识到Op过多问题,于是使用ops_to_register.h来减少Op,进而裁剪TensorFlow包。这方法原理是什么,先让看把Op注册向系统的宏。
  1. #define REGISTER_KERNEL_BUILDER_UNIQ(ctr, kernel_builder, ...)        \
  2.   constexpr bool should_register_##ctr##__flag =                      \
  3.       SHOULD_REGISTER_OP_KERNEL(#__VA_ARGS__);                        \
  4.   static ::tensorflow::kernel_factory::OpKernelRegistrar              \
  5.       registrar__body__##ctr##__object(                               \
  6.           should_register_##ctr##__flag                               \
  7.               ? ::tensorflow::register_kernel::kernel_builder.Build() \
  8.               : nullptr,                                              \
  9.           #__VA_ARGS__,                                               \
  10.           [](::tensorflow::OpKernelConstruction* context)             \
  11.               -> ::tensorflow::OpKernel* {                            \
  12.             return new __VA_ARGS__(context);                          \
  13.           });
复制代码
  一个Op的代码分两部分,一是注册向OpRegistry的代码,它们被放在core/ops目录,二是定义一个从OpKernel派生的类,这是Op代码主要部分,它们被放在core/kernels目录。对OpKernel派生类,它要发生作用需向系统(GlobalKernelRegistry())注册(REGISTER_KERNEL_BUILDER)自个,注册时一定会调用上面定义的REGISTER_KERNEL_BUILDER_UNIQ宏。
  该宏两条语句,第一条定义一个_flag布尔变量,第二条语句定义类实例。注意定义类实例的第一个参数,当_flag变量是true时,传入的值将是kernel_builder构建的对象,是false时则总是nullptr!于是可以反推出,如果一个Op要放入最终代码,那让它对应的_flag是true,否则false。什么决定_flag值?第一条语句说明了是个叫SHOULD_REGISTER_OP_KERNEL的宏,该宏只一个参数,可认为就是Op名称。举个例子,对“ConstantOp”这个Op,如果app想链接它,就让SHOULD_REGISTER_OP_KERNEL("ConstantOp")返回true,否则false。接下让看定义
  1. SHOULD_REGISTER_OP_KERNEL的头文件:selective_registration.h。

  2. #ifdef SELECTIVE_REGISTRATION
  3. #include "ops_to_register.h"
  4. #else
  5. #define SHOULD_REGISTER_OP(op) true
  6. #define SHOULD_REGISTER_OP_GRADIENT true
  7. #define SHOULD_REGISTER_OP_KERNEL(clz) true
  8. #end
复制代码
  可以看出有两种方法定义SHOULD_REGISTER_OP_KERNEL,区分依据是是否有定义SELECTIVE_REGISTRATION宏。一旦定义该宏,用户必须提供ops_to_register.h,由这文件定义具体Op对应的SHOULD_REGISTER_OP_KERNEL。否则对任何Op,SHOULD_REGISTER_OP_KERNEL总是返回true,即只要编译了*.cc,该cc定义的Op全部通过注册,当然这种方法就不需要ops_to_register.h了。
  ops_to_register.h不在TensorFlow源码树,它由用户根据哪个模型产生。
  1. bazel-bin/tensorflow/python/tools/print_selective_registration_header --graphs=tensorflow_inception_graph.pb > ops_to_register.h
复制代码
  print_selective_registration_header是TensorFlow提供的实用程序,上面命令分析tensorflow_inception_graph.pb,搜出当中用到的Op,然后产生ops_to_register.h,在这头文件写上SHOULD_REGISTER_OP对这些Op要返回true。
  使用这个方法是能减少最终app尺寸,但它存在问题:依旧会编译Op所在的*.cc,编译那些*.cc浪费时间是小事,依旧会让Visual Studio就没法编译成功Debug版。——要解决问题,对不须要Op只是不链接是不够的,必须做到不编译*.cc。以下是我正在采用的办法。
一、根据*.pb找到要用到的Op,生成std::set<std::string> ops_。
  1. for (int n = 0; n < graph.node_size(); n++) {
  2.         const tensorflow::NodeDef& node_def = graph.node(n);
  3.         file.ops.insert(node_def.op());
  4. }
复制代码
  上面这个for循环搜索计算图中所有节点,节点对应的Op名称放入它的ops。一个app可能用到多个*.pb,要用到的ops_是这些*.pb的合集。
二、搜索core/kernels下*.cc,生成std::map<std::string, std::set<std::string> > kernels_files_
  kernes_files_的first是*.cc文件名,second是该cc定义了的ops。搜索方法是用以下正则表达式。
  1. REGISTER_KERNEL_BUILDER\([\s\\]*Name\("(.+?)"\)

  2. REGISTER[2-9]?\((.+?),(.+?),\s*"(.+?)",
复制代码
  表达式1就是在该cc定义了的Op。
三、搜索core/ops下*.cc,生成std::map<std::string, std::set<std::string> > ops_files_
  ops_files_的first就是*.cc文件名,second是该cc定义了的ops。搜索方法是用以下正则表达式。
  1. REGISTER_OP\([\s\\]*"(.+?)"\)
复制代码
  子表达1就是在该cc定义了的Op。
四、根据ops_、ops_files_、kernels_files_计算出selected_kernels_、selected_ops_。
  selected_kernels_、selected_ops_变量类型都是std::set<std::string>,它们指示了此次要编译的kernels目录下那些*.cc,ops目录下的那些*.cc。
  到此计算出了要编译的Op相关的cc文件,由这些文件就可构造各平台下的工程文件了。但是,步骤二和步骤三搜索Op可能存在遗漏,同时可能存在A.cc会依赖一个没有定义op的a.cc,它们会造成链接时未定义符号错,对这些错误那只能是手工排除了,排除方法就是增加该“未定义符号”所在的cc文件。
  为简化以上过程,Studio增加了一条叫“Tensorflow工程...”的命令。一旦执行该命令,根据该app拥有的模型会自动生成相应的Windows、iOS、Android下的TensorFlow工程文件。Studio如何判断该app使用了哪些模型?——搜索<res>/app-<app>/tensorflow目录,发现扩展名是*.pb的认为是GraphDef模型文件,扩展名是*.ckpt.meta则是MetaGraphDef。针对步骤二和步骤三可能产生文件遗漏问题,该命令会解析<src>/apps/external/tensorflow/app.cfg。
  1. [tfsmart]
  2.         [insert]
  3.                 no001 = core/framework/resource_handle.pb.cc
  4.                 no002 = core/kernels/bias_op.cc
  5.         [/insert]
  6. [/tfsmart]
复制代码
  块名tfsmart是app标识,指示了要影响到的app。[insert]块存放着要额外编译的*.cc,它们可以是非kerneals、ops目录下的。

初始化静态库的全局变量
  在说这问题前先让看TensorFlow会以什么样形式链入app。
  Windows:静态库。有时须要编译出支持调试的TensorFlow,比如1)用断点查找和Tensorflow相关的Bug,2)要用断点学习TensorFlow源码。但支持调试时会带来副作用,1)大幅增加生成文件尺寸,保守估计,要支持调试的生成文件一般是不支持调试的10倍。2)花费更多的编译时间,原因和第一点有关,既然要生成10倍文件,生成它时自然要花更多时间。由于支持调试有这些副作用,而Windows编译的app默义支持调试,那最好把app和TensorFlow分开,这样可缩小编译花费的时间。
  iOS:静态库。按Rose开发规则,iOS和Android是在要发布时编译生成发布包,编译次数不多,花在编译上时间长点并没什么。当前仍用静态库原因是还没找到好的方法自动生成整个project.pbxproj文件。
  Android:直接链入app。Android平台上编译次数不多,而且已经知道如何生成Android.mk,直接链入app能简化开发者使用TensorFlow。
  综上所述,要同时支持Windows、iOS和Andorid,TensorFlow源码须要支持自个是以静态库的形式链入app。一旦是静态库方法,这就须要注意静态库中全局变量有可能不被初始化(生成)。更具体说,如果静态库中包含一些全局变量,但是从main函数中并不可达,链接器会认为这些变量不起作用,而忽略了它们的初始化,即最终生成的app将不会有这些变量。
  具体例子让看上面的“REGISTER_KERNEL_BUILDER_UNIQ”宏定义,该宏会生成一个叫OpKernelRegistrar的全局变量,它是static,于是不同文件都可用这变量名。也就是说,OpKernel派生类向系统注册自个发生的条件是因为OpKernelRegistrar这个全局变量,一旦最终app忽略产生这变量,该OpKernel就不会被注册,app将不支持该Op。
  类似OpKernel派生类的自注册,OpRegistry自注册也是通过静态变量。除了和Op相关的两种自注册,还有两个地方在使用全局变量实现注册。1)common_runtime/direct_session.cc,向系统注册DirectSession。2)common_runtime/threadpool_device_factory.cc,向系统注册在CPU执行的device_factory。
  如何在app生成这些和注册相关的全局变量?首先会想到的是让main函数可达的地方使用这些变量,这样一来要改TensorFlow的地方就多了。这里运用C/C++语言的链接规范,链接是以目标文件(*.o/*.obj)为模块,只要main可达的地方使用了这目标文件中一个变量或函数,那就会链接整个目标文件。Rose正采用办法是在相关源文件增加一个函数,然后在main可达的地方调用这函数。举个例子,kernels/constant_op.cc要注册“Const”这个op,那在constant_op.cc增加以下代码。
  1. void tensorflow_link_kernels_concat_op() {}
复制代码
  然后在main可达的地方调用tensorflow_link_kernels_concat_op即可。
  Studio提供的“Tensorflow工程...”命令会自动完成以上过程。1)在全局变量所在的.cc文件增加函数。2)会生成一个叫tensorflow_link.hpp的头文件,该文件定义了一个叫tensorflow_link_modules()的函数,该函数执行的就是调用那些个增加的函数。——有了以上这些操作,app要做的是让main能调用tensorflow_link_modules(),Rose建议把它放在game_instance::app_tensorflow_link()。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|丽谷软件|libsdl.cn

GMT+8, 2024-5-17 00:49 , Processed in 0.059297 second(s), 20 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表