欢迎学习ncnn系列相关文章,从训练、模型转换、精度分析,评估到部署Android端,推荐好资源:
一、YoloV5训练自己数据集并测试
二、ncnn编译和安装
三、onnx模型转ncnn模型并推理可执行程序(resnet18例子)
四、yolov5-6.0Pyotorch模型转onxx模型再转ncnn模型部署
五、训练自己YOLOv5模型转ncnn模型并部署到Android手机端
ncnn为用户提供了一系列的模型转换工具,能够轻松地将caffe、onnx等格式的模型转换为ncnn可以识别的格式。在源代码编译完成后,这些工具会被存放在ncnn/build/tools目录下。在本次的示例中,使用了onnx2ncnn工具,将在pytorch中导出的resnet18的onnx模型转换为ncnn模型,并进行了推理。
目录
一、源码包准备
本教程配套源码包获取方法为文章末扫码到公众号「视觉研坊」中回复关键字:模型部署onnx转ncnn。获取下载链接。
源码包下载解压后的样子如下:
二、环境准备
resnet18使用的是Pytorch模型,所以需要先搭好torch环境,下面是我自己的环境版本号,仅供参考,其它版本也行。
三、Pytorch模型转ONNX模型
3.1 注意事项
在onnx模型转ncnn模型前,需要先将Pytorch模型转为onnx模型,不可以Pytorch模型直接转onnx模型,因为这两种模型的格式和结构差异很大。
3.2 代码
具体转换脚本在源码在目录onnx_to_ncnn/python/export_res18.py
import torch
import torchvision.models as models
import torch.onnx as onnx
# 加载预训练的ResNet-18模型
resnet = models.resnet18(pretrained=True) # 使用Pytorch官方的模型加载预训练权重
# 将模型设置为评估模式
resnet.eval()
# 创建一个示例输入张量
dummy_input = torch.randn(1, 3, 224, 224)
# 使用torch.onnx.export函数导出模型为ONNX格式
onnx_file_path = "./model_param/resnet18_test.onnx" # 导出路径 model_param
onnx.export(resnet, dummy_input, onnx_file_path)
print("ResNet-18模型已成功导出为ONNX格式:", onnx_file_path)
3.3 参数修改
3.4 代码运行结果
四、onnx转ncnn
4.1 添加可执行权限
如果电脑上第一次转换,先确保 onnx2ncnn 这个文件有可执行权限,使用 chmod 命令来添加可执行权限:
sudo chmod +x bin/onnx2ncnn
4.2 转换过程
在终端使用下面命令转换:
bin/onnx2ncnn model_param/resnet18_test.onnx model_param/resnet18_test.param model_param/resnet18_test.bin
上面命令中,使用 onnx2ncnn 工具,将 resnet18_test.onnx 转换为 resnet18_test.param 和 resnet18_test.bin。在这里resnet18_test.param 存储了模型的参数信息,它记录了计算图的结构。而 resnet18_test.bin 则存放了模型的所有具体参数。就可以使用 ncnn 框架来加载和运行这个模型了。
4.3 转换结果
下面是具体的转换过程及结果:
4.3.1 .bin文件
bin文件中存放数据,如卷积数据,算子加载的权重等都放在二进制bin文件中。
4.3.2 .param文件
下面是.param文件中具体参数解析,7767517 是一个magic数,表示ncnn格式的;58是layer数,及算子的数量;66是blob数,blob中间数据存储的数量,中间数和计算结果都是放在blob里的。
4.3.3 可视化网络结构
netron官网:entron
在netron中打开.param文件查看计算图结构,方形框中的内容就是layer(算子),各个方形框之间的连接线就是blob。如下:
通过上面的一系列操作就得到了ncnn可以识别的两个文件:.param和.bin。
五、构建和编译
5.1 推理脚本
5.1.2 参数修改
推理脚本为resnet18.cpp,使用是需要修改的地方见下:
5.1.3 代码
#include "net.h"
#include <algorithm>
#if defined(USE_NCNN_SIMPLEOCV)
#include "simpleocv.h"
#else
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#endif
#include <stdio.h>
#include <vector>
static int detect_resnet18(const cv::Mat& bgr, std::vector<float>& cls_scores)
{
ncnn::Net resnet18; // 定义了网络结构
resnet18.opt.use_vulkan_compute = true; // 使用vulkan加速
//分别加载模型的参数和数据
if (resnet18.load_param("model_param/resnet18_test.param"))
exit(-1);
if (resnet18.load_model("model_param/resnet18_test.bin"))
exit(-1);
//opencv读取图片是BGR格式,我们需要转换为RGB格式
ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, bgr.cols, bgr.rows, 224, 224);
//图像归一标准化,以R通道为例(x/225-0.485)/0.229,化简后可以得到下面的式子
//需要注意的式substract_mean_normalize里的方差其实是方差的倒数,这样在算的时候就可以将除法转换为乘法计算
//所以norm_vals里用的是1除
const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
in.substract_mean_normalize(mean_vals, norm_vals);
/*imagenet图片三通道的均值和标准差分别是mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]。以R通道为例,原始图片的像素值是从0到255,
所以像素值归一化即像x/255,减去均值再除以标准差就是(x/255-0.485)/0.229,把255乘下去也就是(x-0.485×255)/255×0.229。如果把归一化和标准
化一起处理的话,等价均值就是0.485×255,等价标准差就是255×0.299。但由于substract_mean_normalize里的标准差实际是标准差的倒数,这样可以把除法
转换为乘法来计算加快效率,所以这里norm_vals用的是标准差的倒数。*/
ncnn::Extractor ex = resnet18.create_extractor();
//把图像数据放入input.1这个blob里
ex.input("input.1", in); // 输入放入input.1
ncnn::Mat out;
//提取出推理结果,推理结果存放在191这个blob里
ex.extract("191", out); // 输出放入191
cls_scores.resize(out.w); // 对输出结果进行遍历
for (int j = 0; j < out.w; j++)
{
cls_scores[j] = out[j];
}
return 0;
}
static int print_topk(const std::vector<float>& cls_scores, int topk)
{
// partial sort topk with index
int size = cls_scores.size();
std::vector<std::pair<float, int> > vec;
vec.resize(size);
for (int i = 0; i < size; i++)
{
vec[i] = std::make_pair(cls_scores[i], i);
}
std::partial_sort(vec.begin(), vec.begin() + topk, vec.end(),
std::greater<std::pair<float, int> >());
// print topk and score
for (int i = 0; i < topk; i++)
{
float score = vec[i].first;
int index = vec[i].second;
fprintf(stderr, "%d = %f\n", index, score); // 输出推理结果中,分数最高的前三个类别
}
return 0;
}
int main(int argc, char** argv)
{
if (argc != 2)
{
fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
return -1;
}
const char* imagepath = argv[1];
//使用opencv读取图片
cv::Mat m = cv::imread(imagepath, 1);
if (m.empty())
{
fprintf(stderr, "cv::imread %s failed\n", imagepath);
return -1;
}
std::vector<float> cls_scores;
detect_resnet18(m, cls_scores); // 调用模型
//打印得分前三的类别
print_topk(cls_scores, 3);
return 0;
}
5.2 CMakeLists.txt修改
下面说到的源码路径,指的是安装源码ncnn时的路径,关于安装源码ncnn见我上一篇博文:ncnn编译和安装
5.3 生成构建文件
先在根目录中生成一个build文件夹,然后执行命令构建如下:
cmake ..
5.3.1 cmake …解析
cmake … 是一个在当前目录下执行 CMake 的命令,它会查找上一级目录(由 … 表示)中的 CMakeLists.txt 文件,并生成构建文件。这些构建文件可以被 make、ninja 或其他构建工具使用,来编译和链接你的项目。
5.3.2 构建结果
通过上面命令执行后会在build文件夹中生成相关文件,如下:
5.4 编译
在上面构建文件基础上执行编译命令:
make
bin目录下的resnet18就是编译生成的可执行程序。
六、推理可执行程序
通过cd命令到根目录下,执行推理命令:
bin/resnet18 image/dog.jpg
下面是可执行程序运行后的输出结果,包含了设备信息,每层的推理时间,以及最终的推理结果。
七、总结
以上就是onnx模型转ncnn模型并推理可执行程序的详细过程,其它模型类型转换并推理。
总结不易,多多支持,谢谢!
感谢您阅读到最后!关注公众号「视觉研坊」,获取干货教程、实战案例、技术解答、行业资讯!