使用C语言实现模型的推理(一)

使用C语言实现模型的推理(一)

  • WHY?
  • 思路整理
    • 从怎么把大象放到冰箱里开始
    • 怎么让模型推理跑起来
  • 生成一个模型
  • 理清楚算子之间的依赖关系
    • 获取tensor信息
    • 获取依赖信息
    • 获取模型的运算图
    • 拓扑排序
  • TO DO
  • 其他
    • bias
    • DELEGATE

WHY?

现在推理框架其实已经有很多了,比如 tensorflow、pytorch、onnxruntime,作为一名搞边缘计算的嵌入式工程师,会更喜欢tflite micro、mnn、ncnn这样的用于移动端的推理引擎。因为这可以将 AI 应用于千家万户。然而,在实际应用中会发现,在微控制处理器上部署神经,即使推理引擎使用tflite micro,也不可避免的会占用过多的内存体积,无论是从模型上还是从推理框架上。

比如,只包含几个简单算子,使用-os来编译tflite micro,库体积也要占用几十KB的内存。

因此,我打算用low-level的C语言来实现一套推理引擎,目标是:

  • 【避繁就简】面向微控制处理器,相对简单的神经网络任务
  • 【寸土寸金】占用内存资源最小( Flash 的 text 段和 data 段)实测比tflite micro要小很多
  • 【大道至简】全部聚焦于模型推理计算,不做一丝丝多余的操作

我没有系统的写过推理框架,相当于是从零开始。这一系列博客我会比较详细的记录我的想法、思路和尝试过程,希望对大家有所帮助。

我在整个过程中大量使用了chatGPT来梳理思路和编写代码,向chatGPT致敬

思路整理

从怎么把大象放到冰箱里开始

这是一个经典的幽默悖论问题,一般的回答是分三步:

1. 打开冰箱门。
2. 把大象放进去。
3. 关上冰箱门。

这个问题的幽默之处在于它看似是一个复杂的问题,但实际上的解决方案却非常简单。

在我看来,比较关键的分两个部分,一个是动作的顺序,一个是动作本身

怎么让模型推理跑起来

思路就很简单了:要想跑一个模型,首先要理清楚算子之间的依赖关系,其次要有各个算子的实现

本文先整理前者的内容。

生成一个模型

考虑到实际应用中tflite模型用的比较多,我们先生成一个 tflite 模型。

import tensorflow as tf
from tensorflow.keras import layers
  
# 定义模型输入
input1 = layers.Input(shape=(4,), name='input1')
input2 = layers.Input(shape=(4,), name='input2')
  
# 分别经过全连接层和激活层
x1 = layers.Dense(8, activation='relu', use_bias=True, bias_initializer='ones')(input1)
x2 = layers.Dense(8, activation='relu')(input2)
  
# 二者结果相加
x = layers.Add()([x1, x2])
  
# 最后经过softmax层得到结果
output = layers.Dense(2, activation='softmax', name='output')(x)
  
# 创建模型
model = tf.keras.Model(inputs=[input1, input2], outputs=output)
  
# 编译模型
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
  
# 打印模型的结构
model.summary()
  
# 转换模型为TFLite格式
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS] # 不希望看到DELEGATE操作,在转换和运行模型时不使用硬件加速。只使用TensorFlow Lite内建的操作
tflite_model = converter.convert()
  
# 保存TFLite模型
with open('model_test.tflite', 'wb') as f:
f.write(tflite_model)

其中,model.summary()打印出的模型信息如下:

Model: "model"
__________________________________________________________________________________________________
 Layer (type)                Output Shape                 Param #   Connected to                  
==================================================================================================
 input1 (InputLayer)         [(None, 4)]                  0         []                            
                                                                                                  
 input2 (InputLayer)         [(None, 4)]                  0         []                            
                                                                                                  
 dense (Dense)               (None, 8)                    40        ['input1[0][0]']              
                                                                                                  
 dense_1 (Dense)             (None, 8)                    40        ['input2[0][0]']              
                                                                                                  
 add (Add)                   (None, 8)                    0         ['dense[0][0]',               
                                                                     'dense_1[0][0]']             
                                                                                                  
 output (Dense)              (None, 2)                    18        ['add[0][0]']                 
                                                                                                  
==================================================================================================
Total params: 98 (392.00 Byte)
Trainable params: 98 (392.00 Byte)
Non-trainable params: 0 (0.00 Byte)
__________________________________________________________________________________________________

这样,就得到了一个model_test.tflite文件。
tflite模型可视化

理清楚算子之间的依赖关系

解析tflite模型,我之前都是通过解析模型的FlatBuffers格式来实现。

FlatBuffers:这个开源库最开始是由Google研发的,专注于提供更优秀的性能。
性能更好的原因是:
1.序列化数据访问不经过转换,即使用了分层数据。这样我们就不需要初始化解析器(没有复杂的字段映射)并且转换这些数据仍然需要时间。
2.flatbuffers不需要申请更多的空间,不需要分配额外的对象。

这一次就不打算用这个东东了,简单粗暴一点,写个python脚本实现,顺便还能理解学习一下tflite相关知识。

获取tensor信息

在Python中,可以使用TensorFlow Lite的Interpreter类来获取TFLite模型的各种信息

import tensorflow as tf

# 加载TFLite模型
interpreter = tf.lite.Interpreter(model_path="your_model.tflite")
interpreter.allocate_tensors()

# 获取模型的详细信息
graph = interpreter.get_tensor_details()

# 打印算子信息
for detail in graph:
    print(f"Name: {detail['name']}")
    print(f"Shape: {detail['shape']}")
    print(f"Type: {detail['dtype']}")
    print(f"Index: {detail['index']}")
    print("\n")

在上述代码中,get_tensor_details()方法返回一个字典列表,每个字典对应模型中的一个算子。字典中包含了算子的名称、形状、数据类型和索引等信息。

比如,其中一个detail如下

{'name': 'serving_default_input1:0', 'index': 0, 'shape': array([1, 4], dtype=int32), 'shape_signature': array([-1,  4], dtype=int32), 'dtype': <class 'numpy.float32'>, 'quantization': (0.0, 0), 'quantization_parameters': {'scales': array([], dtype=float32), 'zero_points': array([], dtype=int32), 'quantized_dimension': 0}, 'sparsity_parameters': {}}

比较遗憾的是,这里的detail里面并没有任何算子之间的依赖信息。不过也并不是一无所获,我们至少可以通过len(graph)知道了一共有多少个tensor。

获取依赖信息

那么,怎么看依赖信息呢?这就需要遍历所有的运算节点,对于每个运算节点,遍历它的所有输入和输出,构建了一个邻接表表示的图。

这个图的每个节点对应一个Tensor,每个边对应一个运算节点。

代码如下:

import tensorflow as tf

# 加载TFLite模型
interpreter = tf.lite.Interpreter(model_path="your_model.tflite")
interpreter.allocate_tensors()

# 获取运算图
graph = interpreter.get_tensor_details()

for op in interpreter._get_ops_details():
	print(op)

比如其中一个op的信息是

{'index': 0, 'op_name': 'FULLY_CONNECTED', 'inputs': array([ 1,  3, -1], dtype=int32), 'outputs': array([6], dtype=int32)}

它的inputs的1,3,2分别是这个全连接的输入、权重、偏置的location,由于这个算子没有偏置,所以它的location-1

这个测试模型的op信息如下:

{'index': 0, 'op_name': 'FULLY_CONNECTED', 'inputs': array([ 1,  3, -1], dtype=int32), 'outputs': array([6], dtype=int32)}
{'index': 1, 'op_name': 'FULLY_CONNECTED', 'inputs': array([0, 4, 2], dtype=int32), 'outputs': array([7], dtype=int32)}
{'index': 2, 'op_name': 'ADD', 'inputs': array([7, 6], dtype=int32), 'outputs': array([8], dtype=int32)}
{'index': 3, 'op_name': 'FULLY_CONNECTED', 'inputs': array([ 8,  5, -1], dtype=int32), 'outputs': array([9], dtype=int32)}
{'index': 4, 'op_name': 'SOFTMAX', 'inputs': array([9], dtype=int32), 'outputs': array([10], dtype=int32)}

然而,需要注意的是,这段代码并不能直接显示算子之间的依赖关系
TFLite模型的算子之间的依赖关系是通过模型的拓扑排序来表示的,也就是说,一个算子的输出可能会成为后续算子的输入。要获取这种依赖关系,需要解析模型的FlatBuffers格式。目前,TensorFlow Lite的Python API并没有提供直接获取算子依赖关系的功能。

要对TFLite模型中的算子进行拓扑排序,首先需要获取模型的运算图。

获取模型的运算图

# 构建邻接表表示的图
adjacency_list = {i: [] for i in range(len(graph))}
for op in interpreter._get_ops_details():
    if op['op_name'] != 'DELEGATE':
        for output_tensor_index in op['outputs']:
            if op['op_name'] == 'FULLY_CONNECTED':
                adjacency_list[op['inputs'][0]].append(output_tensor_index)   
                del adjacency_list[op['inputs'][1]]
                if op['inputs'][2] >=0: # 有可能是-1的情况,如偏置不存在
                    del adjacency_list[op['inputs'][2]]
            else:
                for input_tensor_index in op['inputs']:
                    if input_tensor_index >=0: # 有可能是-1的情况,如偏置不存在
                        adjacency_list[input_tensor_index].append(output_tensor_index)

# 打印邻接表
for k, v in adjacency_list.items():
    print(f"Tensor {k} is connected to tensors {v}")

输出结果:

Tensor 0 is connected to tensors [7]
Tensor 1 is connected to tensors [6]
Tensor 6 is connected to tensors [8]
Tensor 7 is connected to tensors [8]
Tensor 8 is connected to tensors [9]
Tensor 9 is connected to tensors [10]
Tensor 10 is connected to tensors []

在这个例子中,首先使用Interpreter类加载了TFLite模型,并获取了模型的运算图。然后,遍历了所有的运算节点,对于每个运算节点,遍历了它的所有输入和输出,构建了一个邻接表表示的图。这个图的每个节点对应一个Tensor,每个边对应一个运算节点。

需要注意的是,权重和偏执之类的参数就不参与临接表了,我只需要输入输出

拓扑排序

使用拓扑排序算法对这个图进行排序。

def topological_sort(graph):
    # 拓扑排序
    visited = {node: False for node in graph}
    stack = []

    for node in graph:
        if not visited[node]:
            dfs(graph, node, visited, stack)

    return stack[::-1]  # 返回反向的栈,以得到正确的顺序

def dfs(graph, node, visited, stack):
    # 深度优先搜索
    visited[node] = True
    for neighbor in graph[node]:
        if not visited[neighbor]:
            dfs(graph, neighbor, visited, stack)
    stack.append(node)

写了几行代码来可视化了这个过程:
算子拓扑排序

得到算子的计算结果:[1, 6, 0, 7, 8, 9, 10]
与下图对应着看,这个计算顺序没有什么问题。
tflite模型可视化
至此,第一步,算子之间的依赖关系已经完成了。

TO DO

接下来就是怎么把大象放到冰箱里了。具体涉及到:

  • 参数提取
  • 内存复用
  • 算子编写与优化
  • 模型量化

这些内容会在接下来几篇博客展开来讲。


其他

bias

我用keras生成tflite的时候,模型里面的bias没有参数,这是因为偏置是0的缘故,bias的location是-1。

解决办法:

x1 = layers.Dense(8, activation='relu', use_bias=True, bias_initializer='ones')(input1)

DELEGATE

interpreter._get_ops_details()读op的时候,读到了

{'index': 5, 'op_name': 'DELEGATE', 'inputs': array([0, 1, 2, 3, 4, 5], dtype=int32), 'outputs': array([10], dtype=int32)}

在TensorFlow Lite中,DELEGATE操作并不是一个真正的操作,而是一个标记,用来表示一部分计算被委托给了其他的硬件加速器,例如GPU或者Neural Networks API(Android的神经网络API)。

DELEGATE操作的出现通常是因为你在转换模型或者运行模型时使用了硬件加速。如果你不希望看到DELEGATE操作,你可以在转换和运行模型时不使用硬件加速。

在转换模型时,你可以通过设置target_spec.supported_opstf.lite.OpsSet.TFLITE_BUILTINS来只使用TensorFlow Lite内建的操作:

converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS] tflite_model = converter.convert()

在运行模型时,可以通过创建一个没有任何委托的解释器来避免使用硬件加速:

interpreter = tf.lite.Interpreter(model_content=tflite_model)

这样,模型就只会使用TensorFlow Lite内建的操作,不会出现DELEGATE操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/333762.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023预警名单

中国科学院文献情报中心期刊分区表-预警名单 2023年预警名单 2021年预警名单 官方没有2022年预警名单 2020年预警名单 每一年都有变化&#xff0c;今年在预警名单&#xff0c;明年可能就不在预警名单了&#xff0c;具体看学校要求&#xff0c;以及入学年份。

定义域【高数笔记】

【定义域】 1&#xff0c;{知识点} 对于一个函数&#xff0c;f(x)&#xff0c;"f"是起到两个作用&#xff0c;第一&#xff0c;是对自变量的范围的约束&#xff0c;第二&#xff0c;是对运算的约束&#xff0c;同一个"f" 就有同一个约束效果 2&#xff0c;…

离散数学学习要点——命题逻辑

文章目录 数理逻辑命题逻辑命题命题的种类命题的表示 逻辑连接词否定联结词合取联结词∧析取联结词∨或异或 条件➡等价&#xff08;双条件&#xff09;联结词↔联结词真值表 命题逻辑中的命题的符号化命题公式及其真值表命题公式真值表 命题公式的等价重言式与重言蕴含式重言式…

TypeScript依赖注入框架Typedi的使用、原理、源码解读

简介 typedi是一个基于TS的装饰器和reflect-metadata的依赖注入轻量级框架&#xff0c;使用简单易懂&#xff0c;方便拓展。 使用typedi的前提是安装reflect-metadata&#xff0c;并在项目的入口文件的第一行中声明import ‘reflect-metadata’&#xff0c;这样就会在原生的R…

大数据工作岗位需求分析

前言&#xff1a;随着大数据需求的增多&#xff0c;许多中小公司和团队也新增或扩展了大数据工作岗位&#xff1b;但是却对大数据要做什么和能做什么&#xff0c;没有深入的认识&#xff1b;往往是招了大数据岗位&#xff0c;搭建起基础能力后&#xff0c;就一直处于重复开发和…

分类预测 | Matlab实现ISSA-SVM基于多策略混合改进的麻雀搜索算法优化支持向量机的数据分类预测

分类预测 | Matlab实现ISSA-SVM基于多策略混合改进的麻雀搜索算法优化支持向量机的数据分类预测 目录 分类预测 | Matlab实现ISSA-SVM基于多策略混合改进的麻雀搜索算法优化支持向量机的数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 基于多策略混合改进的麻…

录课视频太大怎么办?3种方法一键瘦身~

录制视频是现代人常用的一种记录生活的方式&#xff0c;但是视频文件大小往往会很大&#xff0c;不利于存储和分享。为了解决这个问题&#xff0c;我们需要使用视频压缩软件来压缩视频文件大小&#xff0c;以便更方便地存储和分享。 方法一&#xff1a;嗨格式压缩大师 嗨格式压…

Lucas求大组合数C(n,m)%p

将大组合数C&#xff08;n,m&#xff09;%p分解为小组合数C&#xff08;n,m&#xff09;%p乘积的模&#xff0c;n<10^18,m<10^18。 其中求解小组合数可以根据定义式计算&#xff08;质因子分解&#xff09;&#xff0c;也可以通过定义式的变形计算&#xff08;逆元&…

如何使用Portainer部署web站点并实现无公网ip远程访问

文章目录 前言1. 安装Portainer1.1 访问Portainer Web界面 2. 使用Portainer创建Nginx容器3. 将Web静态站点实现公网访问4. 配置Web站点公网访问地址4.1公网访问Web站点 5. 固定Web静态站点公网地址6. 固定公网地址访问Web静态站点 前言 Portainer是一个开源的Docker轻量级可视…

【根据loss曲线看模型微调效果】如何使用loss曲线诊断机器学习模型性能

一、Loss曲线 在模型的预训练或者微调过程中&#xff0c;我们一般通过观察loss曲线来得出模型对于数据集的学习效果等信息。那么我们如何根据loss曲线得到一些信息呢&#xff1f; 通常数据集会被划分成三部分&#xff0c;训练集&#xff08;training dataset&#xff09;、验证…

Flask 项目怎么配置并创建第一个小项目?附上完成第一个小案例截图

目录 1. 为什么要学习 flask&#xff1f; 2. flask 是什么&#xff1f; 3. flask 如何使用&#xff1f; 要安装 Flask&#xff0c;可以按照以下步骤进行&#xff1a; 4. 使用流程 4.1. 新建项目 4.1.1. 打开 pycharm&#xff0c;新建项目 4.1.2. 设置目录&#xff0c;并…

解决系统开发中的跨域问题:CORS、JSONP、Nginx

文章目录 一、概述1.问题场景2.浏览器的同源策略3.解决思路 二、一点准备工作1.创建前端工程12.创建后端工程3.创建前端工程24.跨域问题 三、方法1&#xff1a;使用CORS四、方法2&#xff1a;JSONP五、方法3&#xff1a;Nginx1.安装和启动&#xff08;windows&#xff09;2.使用…

匿名/箭头函数,立即执行函数IIFE;函数声明式和函数表达式

目录 匿名/箭头函数&#xff1a;简洁 继承上一层作用域链的this 不绑定arguments,用rest参数 rest 参数&#xff1a;...真正的数组 因为没有function声明&#xff0c;所以没有原型prototype&#xff0c;所以不能作为构造函数 当函数体只有一句时&#xff0c;可省 return ,…

【状态压缩】【动态规划】【C++算法】691贴纸拼词

作者推荐 【动态规划】【数学】【C算法】18赛车 本文涉及知识点 状态压缩 动态规划 LeetCode:691 贴纸拼词 我们有 n 种不同的贴纸。每个贴纸上都有一个小写的英文单词。 您想要拼写出给定的字符串 target &#xff0c;方法是从收集的贴纸中切割单个字母并重新排列它们。如…

肇庆韶关异形件上门扫描服务龙岗3D抄数画图福田电脑抄数STL转STP

在当今的数字化时代&#xff0c;对于需要精确测量和设计的客户来说&#xff0c;CASAIM中科广电异形件上门扫描及抄数设计是一项非常重要的服务。这项服务不仅可以为客户提供高质量的测量和设计&#xff0c;还可以帮助他们减少时间和成本。 异形件是一种特殊的零件&#xff0c;…

力扣 | 11. 盛最多水的容器

双指针解法–对撞指针 暴力解法public int maxArea1(int[] height) {int n height.length;int ans 0;for (int i 0; i < n; i) {for (int j i 1; j < n; j) {int area Math.min(height[i], height[j]) * (j - i);ans Math.max(ans, area);}}return ans;}双指针解法…

java毕业设计 | springboot二手交易平台 闲置物品商城(附源码)

1&#xff0c;项目背景 1.1 当前的问题和困惑 随着社会发展&#xff0c;网上购物已经成为我们日常生活的一部分。但是&#xff0c;至今为止大部分电商平台都是从人们日常生活出发&#xff0c;出售都是一些日常用品比如&#xff1a;食物、服装等等&#xff0c;并未发现一个专注…

计算机网络-ACL实验

一、NAT实验配置 NAT实验配置 通过基本ACL匹配VLAN 10网段&#xff0c;然后在出口设备NAT转换只要匹配到VLAN10地址则进行转换。 核心交换机 # 配置VLAN和默认路由&#xff0c;配置Trunk和Access接口 interface Vlanif10ip address 192.168.10.254 255.255.255.0 # interface V…

源聚达科技:个人怎么开抖音店铺

随着互联网的发展&#xff0c;电商平台已经成为了人们购物的主要渠道之一。而抖音作为目前最受欢迎的短视频平台之一&#xff0c;也逐渐成为了一个新兴的电商市场。那么&#xff0c;个人怎么开抖音店铺呢?下面就来详细介绍一下。 第一步&#xff1a;注册抖音账号 首先&#xf…

深度学习进行数据增强(实战篇)

本文章是我在进行深度学习时做的数据增强,接着我们上期的划分测试集和训练集来做. 文章目录 前言 数据增强有什么好处&#xff1f; 一、构造数据增强函数 二、数据增强 总结 前言 很多人在深度学习的时候在对数据的处理时一般采用先数据增强在进行对训练集和测试集的划分,…
最新文章