NCNN 源码(1)-模型加载-数据预处理-模型推理

参考 ncnn 第一个版本的代码。

0 整体流程 demo:squeezenet

ncnn 自带的一个经典 demo:squeezenet 的代码:

// 网络加载
ncnn::Net squeezenet;
squeezenet.load_param("squeezenet_v1.1.param");
squeezenet.load_model("squeezenet_v1.1.bin");// 数据预处理
ncnn::Mat in = ncnn::Mat::from_pixels_resize(image.data, ncnn::Mat::PIXEL_BGR, image.cols, image.rows, 227, 227);
const float mean_vals[3] = { 104.f, 117.f, 123.f };
in.substract_mean_normalize(mean_vals, 0);// 网络推理
ncnn::Extractor ex = squeezenet.create_extractor();
ex.input("data", in);
ncnn::Mat out;
ex.extract("prob", out);

上面的代码描述了从模型加载到模型推理的完整过程:

  • 网络加载
    • load_param
    • load_model
  • 数据预处理
    • from_pixels_resize
    • substract_mean_normalize
  • 网络推理
    • create_extractor
    • input
    • extract
  • 每一层的推理(隐含在模型内,这里没有体现出来)

1 模型加载

ncnn::Net squeezenet;
squeezenet.load_param("squeezenet_v1.1.param");
squeezenet.load_model("squeezenet_v1.1.bin");

这里首先新建一个 ncnn::Net 对象,用来记录网络,除此之外,还有 load_paramload_model方法,分别用来加载网络模型的 param(参数) 文件和 bin(模型)文件。

1.1 load_param

param 文件内容:
在这里插入图片描述

在 param 文件中,第一行记录了模型的 layer 和 blob 的数量,后面的每一行分别记录一个 layer 的一些属性信息。

  • 第一行
    • 第一个数:记录该 param 所表示的模型一共有多少 layer,所以如果增删某几行,这里也要对应修改
    • 第二个数,记录该模型有多少个 blob,可以理解成有多少个数据流节点。例如一个卷积就是一个输入 blob 一个输出 blob,数据在 blob 之间流动。
  • 其他行:除了第一行,其他的都是 layer 行,上图用第三行举例,具体行内容依次为:
    • layer_type:该行记录的 layer 对应的类型,例如输入 Input、卷积 Convolution、激活 ReLU、池化 Pooling 等
    • layer_name:该行记录的 layer 的名字,这个可以自己起,模型导出的时候导出工具自动生成的
    • bottom_count:这里的 bottom 表示该 layer 在谁的下面,因此这个参数的含义是前置节点的数量
    • top_count:该 layer 后置节点的数量
    • bottom_name:这个名字的数量由 bottom_count 指示,上图因为 bottom_count 为 1 所以只有一个。这个参数的含义是前置节点的名称
    • blob_name:后置节点的名字
    • 特有参数:这个是该 layer 特有的一些参数。例如卷积有 kernel_size、stride_size、padding_size,Softmax 则需要一个指示维度的参

load_param 流程伪代码:

# layers 列表,存下所有 layer
# blobs 列表,背后维护,为 find_blob 服务layer_count, blob_count = read(param_file) # 读取第一行数据for param_file is not EOF: # 循环读取每一行的layer数据layer_type, layer_name, bottom_count, top_count = read(param_file) # 读取前四个固定参数layer = create_layer(layer_type) # 根据layer类型创建一个layerfor bottom_count:bottom_name = read(param_file) # 读取每一个bottom_nameblob = find_blob(bottom_name) # 查找该 blob,没有的话就要新建一个blob.consumers.append(layer) # 当前层是这个 blob 的消费者,这里的 blob 是前置节点layer.bottoms.append(blob) # 记录前置节点的名称for top_count:blob_name= read(param_file) # 读取每一个 blob_nameblob = find_blob(bottom_name) # 查找该 blob,没有的话就要新建一个blob.producer = layer # 当前层是这个 blob 的生产者,这里的blob是后置节点layer.tops.append(blob) # 记录后置节点的名称layer.param = read(param_file) # 读取该层的一些特殊参数layers.append(layer)

2 数据预处理

ncnn::Mat in = ncnn::Mat::from_pixels_resize(image.data, ncnn::Mat::PIXEL_BGR, image.cols, image.rows, 227, 227);
in.substract_mean_normalize(mean_vals, norm_vals);

数据预处理部分主要是这样的两个函数:

  • ncnn::Mat::from_pixels_resize:将 cv::Mat 格式的image.data转成ncnn::Mat格式,之后将其 resize 到固定的 shape
  • substract_mean_normalize:对输入数据进行减均值、乘以方差的处理
2.1 ncnn::Mat::from_pixels_resize

先对输入的 image 数据进行 resize 处理,之后将 resize 之后的数据转成ncnn::Mat格式。

2.1.1 resize

ncnn::Mat::from_pixels_resize的 resize 处理支持三种格式的图像:单通道的灰度图像 GRAY,三通道的 RGB 和 BGR,四通道的 RGBA。

resize 使用的是双线性插值算法,即 bilinear:

  • 计算 x、y 方向上插值点的位置索引 xofs 和 yofs
  • 计算 x、y 方向上插值点左右的两个插值系数 ialpha 和 ibeta
  • 遍历插值,x 方向上的插值用 xofs 和 ialpha 得到,y 方向上的插值用 yofs 和 ibeta 得到
2.1.2 from_pixels

这里是先申请一块ncnn::Mat的内存,之后再将转换好的数据逐个填进去即可。这里支持三通道、三通道、四通道的图片输入,一些颜色转换 RGB2BGR、RGB2GRAY 这些也都是在这里实现。

RGB 转 GRAY 的实现如下,from_rgb2gray:

static Mat from_rgb2gray(const unsigned char* rgb, int w, int h) {const unsigned char Y_shift = 8;//14const unsigned char R2Y = 77;const unsigned char G2Y = 150;const unsigned char B2Y = 29;Mat m(w, h, 1);if (m.empty())return m;float* ptr = m;int size = w * h;int remain = size;for (; remain > 0; remain--) {*ptr = (rgb[0] * R2Y + rgb[1] * G2Y + rgb[2] * B2Y) >> Y_shift;rgb += 3;ptr++;}return m;
}

代码中,首先定义了转换时 R、G、B 对应要乘的系数,这里用的是整数乘法,所以系数放大了 2 8 2^8 28,因此后面算结果那里再右移回去。后面就是 for 循环遍历每一个 pixel,全部遍历完并把数据写进 ncnn::Mat 就可以了。

2.2 substract_mean_normalize

这个代码同时支持只mean不norm只norm不mean既mean又norm。既 mean 又 norm:

void Mat::substract_mean_normalize(const float* mean_vals, const float* norm_vals) {int size = w * h;for (int q = 0; q < c; q++) {float* ptr = data + cstep * q;const float mean = mean_vals[q];const float norm = norm_vals[q];int remain = size;for (; remain > 0; remain--) {*ptr = (*ptr - mean) * norm;ptr++;}}
}

遍历 Mat 所有数据,减 mean 乘 norm。

3 模型推理

ncnn::Extractor ex = squeezenet.create_extractor();
ex.input("data", in);
ncnn::Mat out;
ex.extract("prob", out);

这里主要是三个步骤:

  • ncnn::Extractor,即 create_extractor,是一个专门用来维护推理过程数据的类,跟 ncnn::Net 解耦开,这个最主要的就是开辟了一个大小为网络的 blob size 的 std::vectorncnn::Mat 来维护计算中间的数据
  • input,在上一步开辟的 vector 中,把该 input 的 blob 的数据 in 放进去
  • extract,做推理,计算目标层的数据
3.1 extract
int Extractor::extract(const char* blob_name, Mat& feat) {int blob_index = net->find_blob_index_by_name(blob_name);if (blob_index == -1)return -1;int ret = 0;if (blob_mats[blob_index].dims == 0) {int layer_index = net->blobs[blob_index].producer;ret = net->forward_layer(layer_index, blob_mats, lightmode);}feat = blob_mats[blob_index];return ret;
}
  • find_blob_index_by_name, 查找输入的 blob 名字在 vector 中的下标
  • 判断blob_mats[blob_index].dims ==0
    • 如果这个 blob 没有计算过,那么该 blob 对应的数据应该是空的,说明要进行推理
    • 如果 blob 计算过,就有数据了,那么 dims 就不会等于 0,不用再算了,直接取数据就可以了
    • net->forward_layer,又回到了ncnn::Netncnn::Extractor也可以从代码中看出来主要就是维护数据
3.2 forward_layer

函数声明:

int Net::forward_layer(int layer_index, std::vector<Mat>& blob_mats, bool lightmode)

三个参数,

  • layer_index,要计算哪一层
  • blob_mats,记录计算中的数据
  • lightmode 配合 inplace 来做一些动态的 release,及时释放内存资源

推理分 layer 是不是一个输入一个输出其它。一个输入一个输出的代码比较简单,先看这个,

// 1. 获取当前层
const Layer* layer = layers[layer_index];// 2. 获取当前层的前置节点和后置节点
int bottom_blob_index = layer->bottoms[0];
int top_blob_index = layer->tops[0];// 3. 前置节点如果没有推理,就先推理前置节点
if (blob_mats[bottom_blob_index].dims == 0) {int ret = forward_layer(blobs[bottom_blob_index].producer, blob_mats, lightmode);if (ret != 0)return ret;
}// 4. 推理当前节点
Mat bottom_blob = blob_mats[bottom_blob_index];
Mat top_blob;
int ret = layer->forward(bottom_blob, top_blob);
if (ret != 0)return ret;// 5. 当前节点的输出送至后置节点
blob_mats[top_blob_index] = top_blob;

从上面的代码接哦股很清晰,是一个递归,当前层需要的输入 blob 还没算,就递归进去算,算完就算当前层,这里layer->forward是每一层的特定实现

再看非一个输入一个输出的复杂一点的:

const Layer* layer = layers[layer_index];// 1. 所有的前置节点里面,没有推理的都先推理
std::vector<Mat> bottom_blobs;
bottom_blobs.resize(layer->bottoms.size());
for (size_t i=0; i<layer->bottoms.size(); i++) {int bottom_blob_index = layer->bottoms[i];if (blob_mats[bottom_blob_index].dims == 0) {int ret = forward_layer(blobs[bottom_blob_index].producer, blob_mats, lightmode);if (ret != 0)return ret;}bottom_blobs[i] = blob_mats[bottom_blob_index];
}// 2. 前置节点推理完成后,推理当前节点
std::vector<Mat> top_blobs;
top_blobs.resize(layer->tops.size());
int ret = layer->forward(bottom_blobs, top_blobs);
if (ret != 0)return ret;// 3. 当前节点的数据送到所有后置节点
for (size_t i=0; i<layer->tops.size(); i++)
{int top_blob_index = layer->tops[i];blob_mats[top_blob_index] = top_blobs[i];
}

多输入多输出,就是所有前置节点都要先算完,然后再算自己,最后给后置节点送数据。

3.3 推理流程总结

ncnn 推理的整体简要流程:

  • 读取 param 和 bin 文件,记录下每一层的 layer、layer 的输入输出节点、layer 的特定参数
  • 推理
    • 维护一个列表用于存所有节点的数据
    • 给输入节点放入输入数据
    • 计算输出节点的 layer
      • 计算 layer 所需的输入节点还没给输入——>递归调用上一层 layer 计算
      • 有输入了——>计算当前 layer
      • 输出结果,数据送入后置节点

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RIFormer:保持你的视觉主干有效但移除令牌混合器

摘要 https://arxiv.org/pdf/2304.05659 本文研究了如何在去除其基本构建块中的标记混合器&#xff08;token mixers&#xff09;的同时保持视觉主干的有效性。标记混合器作为视觉变换器&#xff08;Vision Transformers, ViTs&#xff09;的自注意力机制&#xff0c;旨在实现…

【排序算法】选择排序、堆排序

文章目录 选择排序选择排序的概念选择排序的基本步骤&#xff1a;选择排序的特点选择排序的代码实现&#xff08;C语言&#xff09; 选择排序-优化双向选择排序的步骤 堆堆的基本概念堆排序详细步骤堆排序代码讲解 选择排序 选择排序的概念 选择排序是一种简单直观的排序算法。…

SpringBoot项目编译运行成功,但有些包名类名仍然下划线标红的解决方法 | Idea

目录 问题解决方案&#xff1a;方法一&#xff1a;方法二【我用这个成功的】 问题 如图&#xff0c;成功运行但有些包名类名仍然下划线标红&#xff0c;强迫症抓狂 成功运行&#xff1a; 有些包导入标红&#xff1a; 解决方案&#xff1a; 方法一&#xff1a; 点击fil…

分布式框架 - ZooKeeper

一、什么是微服务架构 1、单体架构 顾名思义一个软件系统只部署在一台服务器上。 ​ 在高并发场景中&#xff0c;比如电商项目&#xff0c;单台服务器往往难以支撑短时间内的大量请求&#xff0c;聪明的架构师想出了一个办法提高并发量&#xff1a;一台服务器不够就加一台&am…

整合SpringSecurity框架经典报错

报错描述Description: Field userDetailsService in com.atguigu.security.config.WebSecurityConfig required a bean of type org.springframe 这是整合SpringSecurity权限认证中经常出现的一个问题&#xff0c;由于SpringSecurity中这个UserDetailsService未找到 解决方案…

【线程】线程的同步---生产消费者模型

本文重点&#xff1a;理解条件变量和生产者消费者模型 同步是在保证数据安全的情况下&#xff0c;让我们的线程访问资源具有一定的顺序性 条件变量cond 当一个线程互斥地访问某个变量时&#xff0c;它可能发现在其它线程改变状态之前&#xff0c;它什么也做不了&#xff0c;…

Java 微服务框架 HP-SOA v1.1.4

HP-SOA 功能完备&#xff0c;简单易用&#xff0c;高度可扩展的Java微服务框架。 项目主页 : https://www.oschina.net/p/hp-soa下载地址 : https://github.com/ldcsaa/hp-soa开发文档 : https://gitee.com/ldcsaa/hp-soa/blob/master/README.mdQQ Group: 44636872, 66390394…

ctf.show---->re2

做题笔记。 下载 查壳 32 ida打开。 WSL先运行一下&#xff1a; &#xff1f; 创建呗。 函数如下&#xff1a; 逻辑很清晰&#xff0c;写脚本咯 &#xff1a; #include <stdio.h> #include <string.h> #include <stdlib.h>int main() {char encode[] &qu…

TCP网络编程概述、相关函数、及实现超详解

文章目录 TCP网络编程概述1. TCP协议的特点2. TCP与UDP的差异3. TCP编程流程 TCP网络编程相关函数详解1. socket()&#xff1a;创建套接字参数说明&#xff1a;返回值&#xff1a;示例&#xff1a; 2. connect()&#xff1a;客户端连接服务器参数说明&#xff1a;返回值&#x…

IDEA去除掉虚线,波浪线,和下划线实线的方法

初次安装使用IDEA&#xff0c;总是能看到导入代码后&#xff0c;出现很多的波浪线&#xff0c;下划线和虚线&#xff0c;这是IDEA给我们的一些提示和警告&#xff0c;但是有时候我们并不需要&#xff0c;反而会让人看着很不爽&#xff0c;这里简单记录一下自己的调整方法&#…

Ubunt系统设置NVIDIA显卡性能模式

文章目录 前言一、了解自己的显卡1、输入nvidia-smi指令搞清楚表中的含义 二、通过英伟达官方设置进行修改1、此时的状态3、改完后变为P0 总结 前言 工欲善其事&#xff0c;那性能直接拉满&#xff0c;宁可累坏显卡&#xff0c;也不能影响自己&#xff0c;首先了解自己的显卡以…

OpenAPI鉴权(二)jwt鉴权

一、思路 前端调用后端可以使用jwt鉴权&#xff1b;调用三方接口也可以使用jwt鉴权。对接多个三方则与每个third parth都约定一套token规则&#xff0c;因为如果使用同一套token&#xff0c;token串用可能造成权限越界问题&#xff0c;且payload交叉业务不够清晰。下面的demo包…

uni-app页面调用接口和路由(四)

文章目录 一、路由二、页面调用接口二、路由跳转1.uni.navigateTo(OBJECT)2.uni.redirectTo(OBJECT)3.uni.reLaunch(OBJECT)4.uni.switchTab(OBJECT)5.uni.navigateBack(OBJECT) 总结 一、路由 路由配置 uni-app页面路由为框架统一管理&#xff0c;开发者需要在pages.json里配…

iOS六大设计原则设计模式

六大设计原则&#xff1a; 一、单一职责原则 一个类或者模块只负责完成一个职责或者功能。 类似于&#xff1a;UIView 和 CALayer 二、开放封闭原则 对扩展开放&#xff0c;对修改封闭。 我们要尽量通过扩展软件实体来解决需求变化&#xff0c;而不是通过修改已有的代码来…

ESP32-WROOM-32 [创建AP站点-客户端-TCP透传]

简介 基于ESP32-WROOM-32 开篇(刚买)&#xff0c; 本篇讲的是基于固件 ESP32-WROOM-32-AT-V3.4.0.0&#xff08;内含用户指南, 有AT指令说明&#xff09;的TCP透传设置与使用 设备连接 TTL转USB线, 接ESP32 板 的 GND&#xff0c;RX2&#xff0c; TX2 指令介绍 注意,下面指…

Facebook Marketplace无法使用的原因及解决方案

Facebook Marketplace是一项广受欢迎的买卖平台&#xff0c;然而&#xff0c;有时候用户可能会遇到无法访问或使用该功能的问题。通常&#xff0c;这些问题可以归结为以下几类原因&#xff1a; 地理位置限制&#xff1a; Facebook Marketplace并非在全球每个地区都可用。在某些…

【C++】C++中如何处理多返回值

十四、C中如何处理多返回值 本部分也是碎碎念&#xff0c;因为这些点都是很小的点&#xff0c;构不成一篇文章&#xff0c;所以本篇就是想到哪个点就写哪个点。 1、C中如何处理多个返回值 写过python的同学都知道&#xff0c;当你写一个函数的返回时&#xff0c;那是你想返回…

MySQL(日志)

日志 日志分为三种&#xff1a; undo log &#xff08;回滚日志&#xff09;&#xff1a;用于事务回滚和MVCC redo log &#xff08;重做日志&#xff09;&#xff1a;用于故障恢复 binlog &#xff08;归档日志&#xff09;&#xff1a;用于数据备份和主从复制 undo log undo…

一个简单的个人博客管理平台适合新手学习(最底下有github链接)

一个简单的个人博客管理平台 欢迎大家一起学习 前端技术栈 vue3jshtmlcsselement-plus 后端技术栈 springbootredismysqlmybatis 首页展示 页面介绍 左边为文章界面点击文字名字就能进行阅读 右边为热门博客功能能够实时统计前五名博客热度并且将前三名展示在卡片中(卡片…

【人工智能学习之常用损失函数浅谈】

【人工智能学习之常用损失函数浅谈】 Focal Loss基本概念Focal Loss的定义作用应用场景 Arc Loss基本概念ArcFace Loss的定义作用应用场景 CenterLoss基本概念Center Loss 的定义作用应用场景实现细节 Cross Entropy Loss (CELoss)基本概念二分类任务多分类任务作用优点缺点应用…