大佬说选择移植Tengine Python API这个任务,一方面是因为他之前做过涉及Python和C++/C交互的开源项目工作,如 MXNet 中DLPack的Python API和他自己的开源项目 MobulaOP,这些工作让他踩了不少的坑;另一方面是因为他认为,了解一个框架需要先把例子跑起来, 就像学习一门新的编程语言要先跑通它的Hello World程序。通过这次任务,可以了解基于Tengine Lite的图像分类实现,对TengineLite有一个初始的、直观的感受。
以下为大佬第一人称自述\~
初探源码
这个任务的工作可以用一句话来概括: 在Tengine跑起Python例子, 再在Tengine Lite跑起同样的例子。
第一步需要找到Tengine Python API的例子。但Tengine没有的Python例子, 也没有Python API的文档。想从单元测试入手,但也没有Python API的单元测试,难怪移植Python API的任务难度比移植C++ API的任务难度高。还有什么方法可以了解Python API的用法呢? 阅读源码。Tengine的Python API放在pytengine文件夹, 里面有八个模块,分别是:base、context、device、 graph、libinfo、node、tengine和tensor,可以重点关注base, graph和tensor。
从base.py源码中可以看出Tengine使用ctypes的形式进行Python和C++/C的交互,把动态链接库libtengine.so读取后保存到变量\_LIB中,通过\_LIB.可以调用Tengine的C API. graph.py实现计算图部分的API,tensor.py实现了作为输入和输出的张量API。
编译和导入模块
找到Python API代码的位置后,编译Tengine,并尝试在Python中导入tengine模块。从libinfo.py中可以了解到,Tengine Python API会在Python API目录和环境变量LD\_LIBRARY\_PATH所指向的目录中,查找动态链接库libtengine.so.这时遇到了第一个Bug,我用的操作系统没有定义LD\_LIBRARY\_PATH这个环境变量,而API里直接用下标访问的形式取这个环境变量的值,出现了KeyError的错误。改成os.environ.get('LD\_LIBRARY\_PATH', '')即可。设置好动态链接库的路径后,可以成功导入pytengine模块了。
编写Python分类示例
Tengine提供C++/C的分类示例代码,在examples目录下,可以作为编写Python例子时的参考。其中,classification.cpp用了Tengine的C++ API,比C API多了Net类的封装,Net类封装了对于计算图Graph的操作。而classification\_old\_api.cpp用的是Tengine的C API. 由于Tengine Python API调用的是Tengine C API,因此可以拿classification\_old\_api.cpp作为参考。Tengine Python API封装得很简洁,很容易能找到每个Python函数调用的C函数。C++分类示例代码和Python API的代码互相对照,就可以写出Python分类示例的代码。写完后就可以尝试运行了。
运行Python分类示例
运行Python分类示例并不顺利,一开始就在构建计算图上出错了。定位到Python API的源码后, 发现是在以下两行出错。
# pytengine/tengine/graph.py:L24-L25
params = [ c_str(item) for item in kwarg]
self.graph = _LIB.create_graph(ctypes.c_void_p(context), c_str(model), *params)
create\_graph有三个参数: 第一个参数context是模型执行的上下文,第二个参数是模型的格式,第三个参数是模型的文件名。这两行代码看起来没什么毛病,和C++例子里的调用方式是一模一样的。但出错的原因就在给变量params赋值的这一行,里面的item是一个临时变量,当这条语句结束时,item离开了作用域就被释放了。而c\_str(item)是指向原来item的位置,变成了一个野指针。为了解决这个问题,可以把该行改为params = [ c\_str(kwarg[i]) for i in range(len(kwarg)) ],此时c\_str(kwarg[i])指向的是变量kwargs中存储的值,变量kwargs在调用函数create\_graph时仍在作用域内。
另外遇到的一个复杂的问题和Tensor类有关, pytengine的Tensor类还不完善,无法取出Tensor里的数据.。对照C语言写的例子修改Tensor的buf函数,得到数据内存地址,占用的内存大小,类型,尺寸后,转换为NumPy数组。 需要注意一下Tengine Lite前端的执行步骤:
# 建立计算图并读取模型文件
tm_file graph = tg.Graph(None, 'tengine', tm_file)
# 取出输入
Tensor input_tensor = graph.getInputTensor(0, 0)
# 设置输入Tensor的尺寸
dims = [1, 3, img_h, img_w]
input_tensor.shape = dims
# 预先运行以分配资源, 必须加上
graph.preRun()
# 设置输入数据的内存地址, 这里的data是尺寸为(3, img_h, img_w)的NumPy数组. 注意: 这里不会检查shape
input_tensor.buf = data
# 以同步方式让网络进行推断(前向传播), 其中1表示使用同步的方式
graph.run(1) # 1 is blocking
# 取出输出的Tensor
output_tensor = graph.getOutputTensor(0, 0)
# 将Tensor转为NumPy数组
output = np.array(output_tensor.buf)
把这些问题解决后,就能在Tengine上运行Python示例了。
需要注意的是,Tengine Lite和Tengine在做推断前,都需要调用preRun()函数对资源进行分配,这是必须要调用的。 虽然现在的Python API用起来有点复杂,但相信之后会封装得更好的。
从Tengine到Tengine Lite
在Tengine上成功运行Python示例后,移植就变得方便了。直接把pytengine文件夹下的所有代码, 以及Python分类示例复制粘贴到Tengine Lite中,将动态库名称从libtengine.so改为libtengine-lite.so, 然后运行Python分类示例。不出意料,出错了。 原因是Tengine Lite在设置输入Tensor的数据内存地址时,也会检查数据的大小,而之前的Python API的数据大小的计算是错误的。 修复Bug后, 成功在Tengine Lite上运行图像分类示例。移植完成。不得不夸一下Tengine Lite的C API兼容性做得真好!
在EAIDK-310上运行Tengine Lite的Python图像分类示例
之前参加OPEN AI LAB的活动,得到了一块EAIDK-310开发板, 刚好可以在上面进行测试。
这里使用可爱的虎猫(Tiger Cat)作为测试图片,模型采用MobileNet。 图片和模型都可以在Tengine项目的页面中找到链接 (Tengine快速上手指南)。
- 下载代码
[openailab@localhost proj]$ git clone https://github.com/OAID/Tengine
# 进入Tengine的目录
cd Tengine
[openailab@localhost Tengine]$ git branch
* tengine-lite
当前Tengine的默认分支是Tengine Lite。
2. 编译Tengine Lite
mkdir build
cd build
cmake ..
make -j2
注意不要把编译线程数设太大,因为在最后编译MobileNet SSD例子时消耗显存比较多。六分钟多可以编译完。
3. 配置Tengine Lite的Python API编辑/home/openailab/.bashrc,再最后一行后面加入:
export TENGINE_LITE_PATH=/home/openailab/proj/Tengine
export PYTHONPATH=$PYTHONPATH:$TENGINE_LITE_PATH/pytengine
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$TENGINE_LITE_PATH/build/src/
环境变量TENGINE_LITE_PATH
设置为Tengine Lite的根目录路径, 设置好后重新打开终端。 打开Python, 能成功导入tengine。
[openailab@localhost examples]$ python
Python 3.6.5 (default, Mar 29 2018, 17:45:40)
[GCC 8.0.1 20180317 (Red Hat 8.0.1-0.19)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tengine
4. 将三个文件cat.jpg\, mobilenet.tmfile\, synset\_words.txt放在examples的目录下, 文件结构如下所示:
[openailab@localhost examples]$ pwd
/home/openailab/proj/Tengine/examples
[openailab@localhost examples]$ tree
.
├── cat.jpg
├── classification.py
├── mobilenet.tmfile
├── synset_words.txt
5. 运行examples文件夹下的图像分类示例classification.py
[openailab@localhost examples]$ python classification.py
n02123159 tiger cat 8.5975923538208
n02119022 red fox, Vulpes vulpes 7.954988956451416
n02119789 kit fox, Vulpes macrotis 7.867891311645508
n02113023 Pembroke, Pembroke Welsh corgi 7.427407264709473
n02123045 tabby, tabby cat 6.364651679992676
由此,就能够在Tengine Lite上正确预测出虎猫啦 : )
下一步工作
对于Tengine Lite Python API,我觉得在API设计方面可以进一步改进。比如把数据预处理, 计算图构建等操作隐藏起来,比如:
image = cv2.imread('./cat.jpg')
model = tg.Model(tm_file)
pred = model(image)
这样可以减少出错概率, 一些错误比如忘记对数据做预处理,使用的数据内存分布(NCHW还是NHWC)不正确,忘记调用prerun。
本次Tengine Python API移植任务,大佬做的工作是编写一个Python的图像分类示例,在Tengine上跑通代码,再将pytengine移植(复制)到Tengine Lite上, 再在Tengine Lite上跑通代码,其中还修复了pytengine中的一些bug。
更多Tengine相关内容请关注Tengine-边缘AI推理框架专栏。