1 引子:12行
源于 人工智能 的时代潮流,不少同学都在不同平台使用过一些深度学习的前向计算框架(比如tensorflow,caffe,ncnn,pytorch等)。用归用,但框架的内部究竟是如何设计和运作的。
使用ncnn进行前向计算的步骤很简单,就如下十几行代码即可完成。
/* Step1.1 : 加载.parma 文件 */
NSString *paramPath = [[NSBundle mainBundle] pathForResource:@"squeezenet_v1.1" ofType:@"param"];
ncnn_net.load_param(paramPath.UTF8String);
/* Step1.2 : 加载.bin 文件 */
NSString *binPath = [[NSBundle mainBundle] pathForResource:@"squeezenet_v1.1" ofType:@"bin"];
ncnn_net.load_model(binPath.UTF8String);
/* Step2.1 : 构建并配置 提取器 */
ncnn::Extractor extractor = ncnn_net.create_extractor();
extractor.set_light_mode(true);
/* Step2.2 : 设置输入(将图片转换成ncnn::Mat结构作为输入) */
UIImage *srcImage = [UIImage imageNamed:@"mouth"];
ncnn::Mat mat_src;
ts_image2mat(mat_src, srcImage);
extractor.input("data", mat_src);
/* Step2.3 : 提取输出 */
ncnn::Mat mat_dst;
extractor.extract("prob", mat_dst);
如果你仅仅想使用ncnn,上面的参考足够了;但若你想要了解,甚至去更改一些其中的源代码,可以跟我一起看看上面这十多行代码的底层运作原理。
2 代码分析
我姑且将其分为:加载模型、前向检测、输出处理(半划水)、模型封装(全划水) 四个部分来加以分析。
2.1 加载模型
ncnn 在 iOS 端使用 .param 和 .bin 两个文件来描述一个神经网络模型,
其中:
.param:描述神经网络的结构,包括层名称,层输入输出信息,层参数信息(如卷积层的kernal大小等)等。
.bin 文件则记录神经网络运算所需要的数据信息(比如卷积层的权重、偏置信息等)
2.1.1 load_param 加载神经网络配置信息
/* Step1.1 : 加载.parma 文件 */
NSString *paramPath = [[NSBundle mainBundle] pathForResource:@"squeezenet_v1.1" ofType:@"param"];
ncnn_net.load_param(paramPath.UTF8String);
load_param的根本目的是将.param文件的信息加载到目标神经网络(一个ncnn::Net结构)中
2.1.1.1 .param文件的结构
首先我们看一下 .param 文件的内容格式
一个.param文件由以下几部分组成:
1)MagicNum
固定位7767517,为什么这个数字,不知道问倪神去吧
2)layer、blob个数
上图示例的文件两个数字分别为:75、83
layer:我们知道神经网络是一层一层向前推进计算的,每一层我们用一个layer表示;
blob:每一个layer都可能会有输入、输出,在ncnn中,它们统一用一个多维(3维)向量表示,我们称每一个输入、输出的原子为一个blob,并为它起名。
2.1.1.2 layer的描述
layer 在 .param 中是一个相对复杂的元素(从第3行起的每一行描述一个layer),所以我们把它单独抽出来一小节进行说明。
如图,每一行层描述的内容包括以下几部分:
1)层类型
比如Input、Convolution、ReLU
2)层名
模型训练者为该层起得名字(毕竟相同类型的层可能多次使用,我们要区分它们)
3)层输入输出
包含:层输入blob数量,层输出blob数量,层输入、输出blob的名称
4)层配置参数
比如 卷积层(Convolution Layer)的 卷积核大小、步长信息 等
2.1.1.3 ncnn的加载的效果
其实了解了param文件的数据结构后,我们就大致知道ncnn做了哪些事情了。无非是读取文件-->解析神经网络信息-->缓存神经网络信息,那么,信息缓存在哪里呢?
/* in net.h */
class Net
{
...
protected:
std::vector<Blob> blobs;
std::vector<Layer*> layers;
...
};
原来,ncnn::Net 结构中有 blobs 和 layers 两个 vector,它们保存了 .param文件 中加载的信息。关于 Blob、Layer 的数据结构,在此暂不赘述。(自己看代码呗?。?/p>
2.1.2 load_model 加载模型训练数据
/* Step1.2 : 加载.bin 文件 */
NSString *binPath = [[NSBundle mainBundle] pathForResource:@"squeezenet_v1.1" ofType:@"bin"];
ncnn_net.load_model(binPath.UTF8String);
load_model的根本目的是将 .bin文件 的信息加载到 目标神经网络(一个ncnn::Net结构)中。
2.1.2.1 .bin文件的内容
.bin 文件存储了对应模型中部分层的计算需求参数。
比如2.1.1.1节中的 第四行的Convolution层
.bin 文件中就存储了其 1728(3 * 3 * 3 * 64) 个float类型的 权重数据(weight_data) 和 64个float类型的 偏置数据(bias_data)。
2.1.2.2 .bin文件的结构
bin = binary,.bin 文件的基本结构就是 [二进制]
但这 并不代表我们失去了 [手动修改它] 的权利!
惊不惊喜,意不意外?下节即揭晓!
2.1.2.3 手撕二进制
1)bin文件信息存储说明
假设 bin 文件存储 0.3342, 0.4853, 0.2843, 0.1231 四个数字,这四个数字使用float32的数据结构来描述,分别为:3eab1c43、3ef8793e、3e918fc5、3dfc1bda,那么bin文件中的内容就是 3eab1c433ef8793e3e918fc53dfc1bda,我们进行读取的时候使用一个float的数组去承载这些二进制数据即可。
2)手撕
你当然也可以自己写一段bin文件数据的读取方法,比如这么一段
const void * __log_binInfo_conv1(const void *dataOffset) {
printf("\n【conv1】层类型为【Convolution】(卷积层)\n"
"参数配置 0=64 1=3 2=1 3=2 4=0 5=1 6=1728,即:\n"
"输出单元 数量: 64\n"
"核 大小: 3, 3\n"
"核 膨胀: 2, 2\n"
"Pad 大小: 0, 0\n"
"是否有偏置项: 1(是)\n"
"权重数据 数量: 1728 (= 3(核高) * 3(核宽) * 3(RGB三通道) * 64(输出单元数量)\n");
printf("\n【conv1】Load1_1: 加载weight_data数据类型标志(固定为自动类型)\n");
unsigned char *p_load1_1 = (unsigned char *)dataOffset;
for (int i = 0; i < 4; i++) {
printf("Flag %d : %d\n", i, p_load1_1[i]);
}
p_load1_1 += 4;
printf("\n【conv1】Load1_2: 加载weight_data数据(1728项,自动为float32类型)\n");
float *p_load1_2 = (float *)p_load1_1;
for (int i = 0; i < 1728; i++) {
if (i < 10 || i > 1720) {
printf("Weight %d : %.9f\n", i, p_load1_2[i]);
}
}
p_load1_2 += 1728;
printf("\n【conv1】Load2: 加载bias偏置数据(64项,固定为float32类型)\n");
float *p_load2 = (float *)p_load1_2;
for (int i = 0; i < 64; i++) {
if (i < 5 || i > 60) {
printf("Bias %d : %.9f\n", i, p_load2[i]);
}
}
p_load2 += 64;
return p_load2;
}
Demo:
https://github.com/chrisYooh/ncnnSrcDemo
1)打开其下的 NcnnSrcDemo 工程
2)进入 ViewController,解除 自定义bin文件加载测试的 注释
/* 自定义 bin 文件加载测试 */
[self loadModel_myAnalysis];
3)运行看看结果吧,也可以用 ncnn的loadModel 去跑,然后打断点看看解读的 .bin 文件数据一致不。
了解了bin文件的信息存储形式,我们当然就可以进行信息修改咯!不同的框架模型进行转化时,就要做这样的事情。
哇,那我们可以 自己写转模型的工具 啦!从技术上说,完全没错!
(当然我们还要补习神经网络中各种层的信息,以及不同框架的数据结构设计)
2.2 Detect 检测
完成了 网络初始化 load_param()、 load_bin()之后,我们可以填写一个输入并使用 网络提取器Extractor 计算输出了。
2.2.1 创建提取器 Extractor
/* Step2.1 : 构建并配置 提取器 */
ncnn::Extractor extractor = ncnn_net.create_extractor();
extractor.set_light_mode(true);
提取器 extractor 使用 目标网络 通过 友元函数 创建实例,因为它需要获取对应神经网络的信息;同时,extractor 还可以自定义部分配置信息。
Extractor含3个关键类变量
1)net: 指向对应网络的指针
2)blob_mats: 计算的过程中存储输入、输出的临时数据
3)opt: 配置参数
2.2.2 extractor.input 配置输入
/* Step2.2 : 设置输入(将图片转换成ncnn::Mat结构作为输入) */
UIImage *srcImage = [UIImage imageNamed:@"mouth"];
ncnn::Mat mat_src;
ts_image2mat(mat_src, srcImage);
extractor.input("data", mat_src);
1)我们要 构造一个ncnn::Mat的结构,将我们的输入填入其中
2)利用 Extractor的input()函数 将输入mat填入对应的位置。
注意:input() 函数中的第一个字符串参数输入的是 blob的名称 而不是 layer的名称 哦?。ㄈ缬杏行┿拢梢曰乜匆幌?[2.1.1.1节] 的 .param的文件描述,区分下 layer 和 blob)
2.2.3 extractor.extract 提取输出
/* Step2.3 : 提取输出 */
ncnn::Mat mat_dst;
extractor.extract("prob", mat_dst);
1)我们要 构造一个ncnn::Mat的结构,用以承载输出;
2)利用 Extractor的extract()函数 将计算结果填写到我们构造的输出mat中。
注意:input()函数中的第一个字符串参数输入的是 blob的名称 而不是 layer的名称 哦?。ㄈ缬杏行┿拢梢曰乜匆幌?[2.1.1.1节] 的 .param的文件描述,区分下 layer 和 blob)
2.2.3.1 extract() 的递归流程图
ncnn 在进行 extract() 的时候,使用了递归的方式,这边将其 宏观逻辑进行抽象。(描绘所有的代码细节会使图过于复杂,不易阅读)
2.2.3.2 extract() 最简递归展开流程
之所以说最简,因为我们假设:
1 目标网络的每层都只有 一个输入(blob) 和 一个输出(blob),
2 使用的extract是新创建的(即 无缓存数据)
如图:
1)每一层进行forward()的时候,需要一些输入参数,这些输入参数是 由上面的层的forward()运算输出的。
2)只有 输入层的输入参数是我们填写的(2.2.2 节),也正是因为它的存在,递归得以有了终结。
2.3 输出处理
/* Step3.1 : 结果处理(获取检测概率最高的5种物品,认为存在) */
NSArray *rstArray = ts_mat2array(mat_dst);
NSArray *top5Array = ts_topN(rstArray, 5);
/* Step3.2 : 打印输出 */
NSLog(@"%@", top5Array);
/* 说明:该Demo中发现输出的第一项是 index 为 673 的项目,
* 在result_info.json中查找下 "index" : "673" 发现对应的描述是 鼠标
* 也可以换其他图片进行检测,但要将图片规格化成 227 * 227 的大小才可以保证结果的准确性
*/
输出处理是根据需求具体模型需求,很灵活的。
比如我给予输出结果的每个数字以 概念(识别到某种物品的概率) ,并对输出结果进行排序后取其 概率最高的 五个值。
2.4 封装
玩一玩的话,12行代码足够了;但若真的要工程化的话,我们还是要将 面向过程 的思路 向 面向对象 靠拢的。
可惜的是,这边只能提点一下 要有封装的意识。
因为在公司领导决定开源我们的SDK之前,不太方便透漏我们相关的封装思路咯。
3 文尾福利 Demo
老套路,文尾送福利!
这边提供一个 Ncnn 的 iOS源码Demo。其中,你可以直接 在ncnn源码中打断点,加日志,通过调试源码的方式对ncnn快速理解。
Github地址:https://github.com/chrisYooh/ncnnSrcDemo
打开其下的 ncnnSrcDemo工程,然后,开始愉快地 Debug 吧!:)
呃……好像最近蛮流行 文章末尾随便塞张图……
你猜我消消乐玩到第几关了???