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

news2024/11/15 11:07:37

参考 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类型创建一个layer

    for bottom_count:
        bottom_name = read(param_file) # 读取每一个bottom_name
        blob = find_blob(bottom_name) # 查找该 blob,没有的话就要新建一个
        blob.consumers.append(layer) # 当前层是这个 blob 的消费者,这里的 blob 是前置节点
        layer.bottoms.append(blob) # 记录前置节点的名称
    
    for top_count:
        blob_name= read(param_file) # 读取每一个 blob_name
        blob = 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;//14
    const 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.coloradmin.cn/o/2155567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

对象关系映射ORM

目录 ORM【重要】 1、 什么是ORM 2、 实体类 3、 ORM改造登录案例 ORM【重要】 1、 什么是ORM 目前使用JDBC完成了CRUD,但是现在是进行CRUD,增删改方法要设计很多参数,查询的方法需要设计集合才能返回. 在实际开发中,我们需要将零散的数据封装到对象处理. ORM (Object Rela…

在曲线图上最值和极值点位置进行适当标注

1、首先生成一组0-100的随机数&#xff0c;组内共有100个数据&#xff1b; yyrandi([0,100],[1,100]); 2、求这组数据的功率谱密度&#xff0c;并绘图&#xff1b; msize(yy,2); xdft fft(yy); % 计算功率谱密度 psd (1/m) * abs(xdft).^2; x1:m; loglog(x,psd,Linewid…

恶意windows程序

Lab07-01.exe分析&#xff08;DOS攻击&#xff09; 1.当计算机重启后&#xff0c;这个程序如何确保它继续运行(达到持久化驻留)? 创建Malservice服务实现持久化 先分析sub_401040桉函数 尝试获取名为HGL345互斥量句柄&#xff0c;如果不存在则直接结束流程&#xff1b;如果存…

【设计模式】万字详解:深入掌握五大基础行为模式

作者&#xff1a;后端小肥肠 &#x1f347; 我写过的文章中的相关代码放到了gitee&#xff0c;地址&#xff1a;xfc-fdw-cloud: 公共解决方案 &#x1f34a; 有疑问可私信或评论区联系我。 &#x1f951; 创作不易未经允许严禁转载。 姊妹篇&#xff1a; 【设计模式】&#xf…

主语部分、谓语部分、限定动词 (谓语动词) 和非限定动词 (非谓语动词)

主语部分、谓语部分、限定动词 {谓语动词} 和非限定动词 {非谓语动词} 1. 主语部分 (subject)1.1. Forms of the subject 2. 谓语部分 (predicate)2.1. Cambridge Dictionary2.2. Longman Dictionary of Contemporary English2.3. 谓语部分和谓语动词2.4. Traditional grammar …

240922-Ollama使用Embedding实现RAG

A. 最终效果 B. 文本分块代码 #%% from PyPDF2 import PdfReader from langchain.text_splitter import CharacterTextSplitterpdf_path 2023-LiuGuokai-Meas.pdf pdf_reader PdfReader(pdf_path) text "" for page in pdf_reader.pages:text page.extract_text…

2024年最新 Python 大数据网络爬虫技术基础案例详细教程(更新中)

网络爬虫概述 网络爬虫&#xff08;Web Crawler&#xff09;&#xff0c;又称为网页蜘蛛&#xff08;Web Spider&#xff09;或网络机器人&#xff08;Web Robot&#xff09;&#xff0c;是一种自动化程序或脚本&#xff0c;用于浏览万维网&#xff08;World Wide Web&#xf…

(学习总结)STM32CubeMX HAL库 学习笔记撰写心得

STM32CubeMX学习笔记撰写心得 引言 在深入学习和实践STM32系列微控制器的开发过程中&#xff0c;我经历了从标准库到HAL库&#xff0c;再到结合STM32CubeMX进行项目开发的转变。这一过程中&#xff0c;我深刻体会到了STM32CubeMX在配置和代码生成方面的强大与便捷。为了检验自…

哈希简单介绍

1.直接定址法&#xff08;值的分布范围集中&#xff09; 比如统计字符串中字符出现的字数&#xff0c;字符范围是集中 2.除留余数法&#xff08;值的分布范围分散&#xff09; hashkey%n 哈希冲突&#xff1a;不同的值映射到相同的位置 解决哈希冲突的方案&#xff1a; 闭散…

抖音短视频矩阵系统OEM源码开发注意事项,功能开发细节流程全揭秘

抖音短视频矩阵系统OEM源码开发注意事项,功能开发细节流程全揭秘 在当今数字化时代背景下&#xff0c;短视频产业正经历前所未有的快速发展。其中&#xff0c;抖音凭借其创新的算法及多元内容生态获得巨大成功&#xff0c;吸引了众多用户。对于意欲进入短视频领域的创业者而言&…

【RocketMQ】一、基本概念

文章目录 1、举例2、MQ异步通信3、背景4、Rocket MQ 角色概述4.1 主题4.2 队列4.3 消息4.4 生产者4.5 消费者分组4.6 消费者4.7 订阅关系 5、消息传输模型5.1 点对点模型5.2 发布订阅模型 1、举例 以坐火车类比MQ&#xff1a; 安检大厅就像是一个系统的门面&#xff0c;接受来…

springboot地方特色美食分享系统-计算机毕业设计源码02383

摘要 本论文主要论述了如何使用SpringBoot技术开发一个地方特色美食分享系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述地方特色美食分享系统的当前背景以…

数据结构与算法——Java实现 8.习题——移除链表元素(值)

祝福你有前路坦途的好运&#xff0c;更祝愿你能保持内心光亮 纵有风雨&#xff0c;依然选择勇敢前行 —— 24.9.22 203. 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示…

uniapp 微信小程序 订阅消息功能实现

该网址 https://api.weixin.qq.com 上线后不可访问&#xff0c;调用该网址操作需在后端&#xff08; 重要&#xff01; 重要&#xff01; 重要&#xff01;&#xff09; 1.首先拿到的三个码 //微信公众平台 //https://mp.weixin.qq.com const wxappid "管理-开发管理-A…

手机号归属地查询-运营商归属地查询-手机号归属地信息-运营商手机号归属地查询接口-手机号归属地

手机号归属地查询接口是一种网络服务接口&#xff0c;它允许开发者通过编程方式查询手机号码的注册地信息。这种接口通常由第三方服务提供商提供&#xff0c;并可通过HTTP请求进行调用。以下是一些关于手机号归属地查询接口的相关信息&#xff1a; 1. 接口功能 归属地查询&am…

51单片机——矩阵键盘

一、矩阵键盘原理图 我们发现: P17,P16,P15,P14控制行&#xff0c; P13,P12,P11,P10控制列。 所以我们如果要选择第四列&#xff0c;只需要把整个P1先给高电位1&#xff0c;再把P10给低电位0。 二、代码 P10xFF; P100; if(P170){Delay(20);while(P170);Delay(20);KeyNum…

EasyExcel根据模板生成excel文件【xls、xlsx】

1、简介 如下图所示&#xff0c;template目录下是准备好的模板&#xff0c;export目录下是生成数据文件。我们这里以第一个模板《theUser蒸汽历史数据.xls》为例进行测试&#xff0c;theUser为占位符&#xff0c;生成的文件中会被替换成对应的用户名。 我这里的代码逻辑是根据…

[Linux]:信号(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;Linux学习 贝蒂的主页&#xff1a;Betty’s blog 1. 信号的阻塞 1.1 基本概念 信号被操作系统发送给进程之后&#xff0c;进程…

语言模型的在线策略提炼:从自我错误中学习

原论文&#xff1a;On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes 摘要 知识蒸馏&#xff08;KD&#xff09;被广泛用于通过训练较小的学生模型来压缩教师模型&#xff0c;以降低推理成本和内存占用。然而&#xff0c;当前用于自回归序…

【笔记】材料分析测试:晶体学

晶体与晶体结构Crystal and Crystal Structure 1.晶体主要特征 固态物质可以分为晶态和非晶态两大类&#xff0c;分别称为晶体和非晶体。 晶体和非晶体在微观结构上的区别在于是否具有长程有序。 晶体&#xff08;长程有序&#xff09;非晶&#xff08;短程有序&#xff09…