【GiantPandaCV导语】自NCNN开源以来,其它厂商的端侧推理框架或者搭载特定硬件芯片的工具链层出不穷。如何去繁从简的阅读一个深度学习推理框架十分重要,这篇文章记录了我是如何阅读NCNN框架的,希望对一些不知道如何下手的读者有一点启发。
0x00. 想法来源
CNN从15年的ResNet在ImageNet比赛中大放异彩,到今天各种层出不穷的网络结构被提出以解决生活中碰到的各种问题。然而,在CNN长期发展过程中,也伴随着很多的挑战,比如如何调整算法使得在特定场景或者说数据集上取得最好的精度,如何将学术界出色的算法落地到工业界,如何设计出在边缘端或者有限硬件条件下的定制化CNN等。前两天看到腾讯优图的文章:腾讯优图开源这三年 ,里面提到了NCNN背后的故事,十分感动和佩服,然后我也是白嫖了很多NCNN的算法实现以及一些调优技巧。所以为了让很多不太了解NCNN的人能更好的理解腾讯优图这个"从0到1"的深度学习框架,我将结合我自己擅长的东西来介绍我眼中的NCNN它是什么样的?
0x01. 如何使用NCNN
这篇文章的重点不是如何跑起来NCNN的各种Demo,也不是如何使用NCNN来部署自己的业务网络,这部分没有什么比官方wiki介绍得更加清楚的资料了。所以这部分我只是简要汇总一些资料,以及说明一些我认为非常重要的东西。
官方wiki指路:https://github.com/Tencent/ncnn/wiki
在NCNN中新建一个自定义层教程:https://github.com/Ewenwan/MVision/blob/master/CNN/HighPerformanceComputing/example/ncnn_%E6%96%B0%E5%BB%BA%E5%B1%82.md
NCNN下载编译以及使用:https://github.com/Ewenwan/MVision/blob/master/CNN/HighPerformanceComputing/example/readme.md
0x02. 运行流程解析
要了解一个深度学习框架,首先得搞清楚这个框架是如何通过读取一张图片然后获得的我们想要的输出结果,这个运行流程究竟是长什么样的?我们看一下NCNN官方wiki中提供一个示例代码:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "net.h"
int main()
{
// opencv读取输入图片
cv::Mat img = cv::imread("image.ppm", CV_LOAD_IMAGE_GRAYSCALE);
int w = img.cols;
int h = img.rows;
// 减均值以及缩放操作,最后输入数据的值域为[-1,1]
ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_GRAY, w, h, 60, 60);
float mean[1] = {
128.f };
float norm[1] = {
1/128.f };
in.substract_mean_normalize(mean, norm);
// 构建NCNN的net,并加载转换好的模型
ncnn::Net net;
net.load_param("model.param");
net.load_model("model.bin");
// 创建网络提取器,设置网络输入,线程数,light模式等等
ncnn::Extractor ex = net.create_extractor();
ex.set_light_mode(true);
ex.set_num_threads(4);
ex.input("data", in);
// 调用extract接口,完成网络推理,获得输出结果
ncnn::Mat feat;
ex.extract("output", feat);
return 0;
0x02.00 图像预处理ncnn::Mat
可以看到NCNN对于我们给定的一个网络(首先转换为NCNN的param和bin文件)和输入,首先执行图像预处理,这是基于ncnn::Mat这个数据结构完成的。
其中,from_pixels_resize() 这个函数的作用是生成目标尺寸大小的网络输入Mat,它的实现在https://github.com/Tencent/ncnn/blob/b93775a27273618501a15a235355738cda102a38/src/mat_pixel.cpp#L2543。它的内部实际上是根据传入的输入图像的通道数完成resize_bilinear_c1/c2/c3/4 即一通道/二通道/三通道/四通道 图像变形算法,可以看到使用的是双线性插值算法。这些操作的实现在https://github.com/Tencent/ncnn/blob/master/src/mat_pixel_resize.cpp#L27。然后经过Resize之后,需要将像素图像转换成ncnn::Mat。这里调用的是Mat::from_pixels()这个函数,它将我们Resize操作之后获得的像素图像数据(即float*数据)根据特定的输入类型赋值给ncnn::Mat。
接下来,我们讲讲substract_mean_normalize()这个函数,它实现了减均值和归一化操作,它的实现在:https://github.com/Tencent/ncnn/blob/master/src/mat.cpp#L34。具体来说,这个函数根据均值参数和归一化参数的有无分成这几种情况:
- 有均值参数
- 创建 偏置层 ncnn::create_layer(ncnn::LayerType::Bias); 载入层参数 op->load_param(pd); 3通道
- 载入层权重数据 op->load_model(ncnn::ModelBinFromMatArray(weights)); -均值参数
- 运行层 op->forward_inplace(*this);
- 有归一化参数
- 创建 尺度层 ncnn::create_layer(ncnn::LayerType::Scale); 载入层参数 op->load_param(pd); 3通道
- 载入层权重数据 op->load_model(ncnn::ModelBinFromMatArray(weights)); 尺度参数
- 运行层 op->forward_inplace(*this);
- 有均值和归一化参数
- 创建 尺度层 ncnn::create_layer(ncnn::LayerType::Scale); 载入层参数 op->load_param(pd); 3通道
- 载入层权重数据 op->load_model(ncnn::ModelBinFromMatArray(weights)); -均值参数 和 尺度参数
- 运行层 op->forward_inplace(*this);
可以看到NCNN的均值和归一化操作,是直接利用了它的Bias Layer和Scale Layer来实现的,也就是说NCNN中的每个层都可以单独拿出来运行我们自己数据,更加方便我们白嫖 。
0x02.01 模型解析ncnn::Net
param 解析
完成了图像预处理之后,新增了一个ncnn::Net,然后调用Net::load_param来载入网络参数文件 *.proto, 这部分的实现在https://github.com/Tencent/ncnn/blob/master/src/net.cpp#L115。在讲解这个函数在的过程之前,我们先来一起分析一下NCNN的param文件,举例如下:
7767517 # 文件头 魔数
75 83 # 层数量 输入输出blob数量
# 下面有75行
Input data 0 1 data 0=227 1=227 2=3
Convolution conv1 1 1 data conv1 0=64 1=3 2=1 3=2 4=0 5=1 6=1728
ReLU relu_conv1 1 1 conv1 conv1_relu_conv1 0=0.000000
Pooling pool1 1 1 conv1_relu_conv1 pool1 0=0 1=3 2=2 3=0 4=0
Convolution fire2/squeeze1x1 1 1 pool1 fire2/squeeze1x1 0=16 1=1 2=1 3=1 4=0 5=1 6=1024
...
层类型 层名字 输入blob数量 输出blob数量 输入blob名字 输出blob名字 参数字典
参数字典,每一层的意义不一样:
数据输入层 Input data 0 1 data 0=227 1=227 2=3 图像宽度×图像高度×通道数量
卷积层 Convolution ... 0=64 1=3 2=1 3=2 4=0 5=1 6=1728
0输出通道数 num_output() ; 1卷积核尺寸 kernel_size(); 2空洞卷积参数 dilation(); 3卷积步长 stride();
4卷积填充pad_size(); 5卷积偏置有无bias_term(); 6卷积核参数数量 weight_blob.data_size();
C_OUT * C_in * W_h * W_w = 64*3*3*3 = 1728
池化层 Pooling 0=0 1=3 2=2 3=0 4=0
0池化方式:最大值、均值、随机 1池化核大小 kernel_size(); 2池化核步长 stride();
3池化核填充 pad(); 4是否为全局池化 global_pooling();
激活层 ReLU 0=0.000000 下限阈值 negative_slope();
ReLU6 0=0.000000 1=6.000000 上下限
综合示例:
0=1 1=2.5 -23303=2,2.0,3.0
数组关键字 : -23300
-(-23303) - 23300 = 3 表示该参数在参数数组中的index
后面的第一个参数表示数组元素数量,2表示包含两个元素
然后官方的wiki中提供了所有网络层的详细参数设置,地址为:https://github.com/Tencent/ncnn/wiki/operation-param-weight-table
了解了Param的基本含义之后,我们可以来看一下Net::load_param这个函数是在做什么了。
从函数实现,我们知道,首先会遍历param文件中的所有网络层,然后根据当前层的类型调用create_layer()/ net::create_custom_layer()来创建网络层,然后读取输入Blobs和输出Blobs和当前层绑定,再调用paramDict::load_param(fp)解析当前层的特定参数(参数字典),按照id=参数/参数数组来解析。最后,当前层调用layer->load_param(pd)载入解析得到的层特殊参数即获得当前层特有的参数。
核心代码解析如下:
// 参数读取 程序
// 读取字符串格式的 参数文件
int ParamDict::load_param(FILE* fp)
{
clear();
// 0=100 1=1.250000 -23303=5,0.1,0.2,0.4,0.8,1.0
// parse each key=value pair
int id = 0;
while (fscanf(fp

1285

被折叠的 条评论
为什么被折叠?



