1 引子:12行
源于 人工智能 的時(shí)代潮流,不少同學(xué)都在不同平臺(tái)使用過(guò)一些深度學(xué)習(xí)的前向計(jì)算框架(比如tensorflow,caffe族跛,ncnn,pytorch等)锐墙。用歸用礁哄,但框架的內(nèi)部究竟是如何設(shè)計(jì)和運(yùn)作的。
使用ncnn進(jìn)行前向計(jì)算的步驟很簡(jiǎn)單溪北,就如下十幾行代碼即可完成桐绒。
/* 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 : 構(gòu)建并配置 提取器 */
ncnn::Extractor extractor = ncnn_net.create_extractor();
extractor.set_light_mode(true);
/* Step2.2 : 設(shè)置輸入(將圖片轉(zhuǎn)換成ncnn::Mat結(jié)構(gòu)作為輸入) */
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,上面的參考足夠了之拨;但若你想要了解茉继,甚至去更改一些其中的源代碼,可以跟我一起看看上面這十多行代碼的底層運(yùn)作原理蚀乔。
2 代碼分析
我姑且將其分為:加載模型烁竭、前向檢測(cè)、輸出處理(半劃水)吉挣、模型封裝(全劃水) 四個(gè)部分來(lái)加以分析派撕。
2.1 加載模型
ncnn 在 iOS 端使用 .param 和 .bin 兩個(gè)文件來(lái)描述一個(gè)神經(jīng)網(wǎng)絡(luò)模型,
其中:
.param:描述神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)睬魂,包括層名稱终吼,層輸入輸出信息,層參數(shù)信息(如卷積層的kernal大小等)等汉买。
.bin 文件則記錄神經(jīng)網(wǎng)絡(luò)運(yùn)算所需要的數(shù)據(jù)信息(比如卷積層的權(quán)重衔峰、偏置信息等)
2.1.1 load_param 加載神經(jīng)網(wǎng)絡(luò)配置信息
/* Step1.1 : 加載.parma 文件 */
NSString *paramPath = [[NSBundle mainBundle] pathForResource:@"squeezenet_v1.1" ofType:@"param"];
ncnn_net.load_param(paramPath.UTF8String);
load_param的根本目的是將.param文件的信息加載到目標(biāo)神經(jīng)網(wǎng)絡(luò)(一個(gè)ncnn::Net結(jié)構(gòu))中
2.1.1.1 .param文件的結(jié)構(gòu)
首先我們看一下 .param 文件的內(nèi)容格式
一個(gè).param文件由以下幾部分組成:
1)MagicNum
固定位7767517,為什么這個(gè)數(shù)字蛙粘,不知道問(wèn)倪神去吧
2)layer、blob個(gè)數(shù)
上圖示例的文件兩個(gè)數(shù)字分別為:75威彰、83
layer:我們知道神經(jīng)網(wǎng)絡(luò)是一層一層向前推進(jìn)計(jì)算的出牧,每一層我們用一個(gè)layer表示;
blob:每一個(gè)layer都可能會(huì)有輸入歇盼、輸出舔痕,在ncnn中,它們統(tǒng)一用一個(gè)多維(3維)向量表示,我們稱每一個(gè)輸入伯复、輸出的原子為一個(gè)blob慨代,并為它起名。
2.1.1.2 layer的描述
layer 在 .param 中是一個(gè)相對(duì)復(fù)雜的元素(從第3行起的每一行描述一個(gè)layer)啸如,所以我們把它單獨(dú)抽出來(lái)一小節(jié)進(jìn)行說(shuō)明侍匙。
如圖,每一行層描述的內(nèi)容包括以下幾部分:
1)層類(lèi)型
比如Input叮雳、Convolution想暗、ReLU
2)層名
模型訓(xùn)練者為該層起得名字(畢竟相同類(lèi)型的層可能多次使用,我們要區(qū)分它們)
3)層輸入輸出
包含:層輸入blob數(shù)量帘不,層輸出blob數(shù)量说莫,層輸入、輸出blob的名稱
4)層配置參數(shù)
比如 卷積層(Convolution Layer)的 卷積核大小寞焙、步長(zhǎng)信息 等
2.1.1.3 ncnn的加載的效果
其實(shí)了解了param文件的數(shù)據(jù)結(jié)構(gòu)后储狭,我們就大致知道ncnn做了哪些事情了。無(wú)非是讀取文件-->解析神經(jīng)網(wǎng)絡(luò)信息-->緩存神經(jīng)網(wǎng)絡(luò)信息捣郊,那么晶密,信息緩存在哪里呢?
/* in net.h */
class Net
{
...
protected:
std::vector<Blob> blobs;
std::vector<Layer*> layers;
...
};
原來(lái)模她,ncnn::Net 結(jié)構(gòu)中有 blobs 和 layers 兩個(gè) vector稻艰,它們保存了 .param文件 中加載的信息。關(guān)于 Blob侈净、Layer 的數(shù)據(jù)結(jié)構(gòu)尊勿,在此暫不贅述。(自己看代碼唄P笳臁)
2.1.2 load_model 加載模型訓(xùn)練數(shù)據(jù)
/* Step1.2 : 加載.bin 文件 */
NSString *binPath = [[NSBundle mainBundle] pathForResource:@"squeezenet_v1.1" ofType:@"bin"];
ncnn_net.load_model(binPath.UTF8String);
load_model的根本目的是將 .bin文件 的信息加載到 目標(biāo)神經(jīng)網(wǎng)絡(luò)(一個(gè)ncnn::Net結(jié)構(gòu))中元扔。
2.1.2.1 .bin文件的內(nèi)容
.bin 文件存儲(chǔ)了對(duì)應(yīng)模型中部分層的計(jì)算需求參數(shù)。
比如2.1.1.1節(jié)中的 第四行的Convolution層
.bin 文件中就存儲(chǔ)了其 1728(3 * 3 * 3 * 64) 個(gè)float類(lèi)型的 權(quán)重?cái)?shù)據(jù)(weight_data) 和 64個(gè)float類(lèi)型的 偏置數(shù)據(jù)(bias_data)旋膳。
2.1.2.2 .bin文件的結(jié)構(gòu)
bin = binary澎语,.bin 文件的基本結(jié)構(gòu)就是 [二進(jìn)制]
但這 并不代表我們失去了 [手動(dòng)修改它] 的權(quán)利!
驚不驚喜验懊,意不意外擅羞?下節(jié)即揭曉!
2.1.2.3 手撕二進(jìn)制
1)bin文件信息存儲(chǔ)說(shuō)明
假設(shè) bin 文件存儲(chǔ) 0.3342, 0.4853, 0.2843, 0.1231 四個(gè)數(shù)字义图,這四個(gè)數(shù)字使用float32的數(shù)據(jù)結(jié)構(gòu)來(lái)描述减俏,分別為:3eab1c43、3ef8793e碱工、3e918fc5娃承、3dfc1bda奏夫,那么bin文件中的內(nèi)容就是 3eab1c433ef8793e3e918fc53dfc1bda,我們進(jìn)行讀取的時(shí)候使用一個(gè)float的數(shù)組去承載這些二進(jìn)制數(shù)據(jù)即可历筝。
2)手撕
你當(dāng)然也可以自己寫(xiě)一段bin文件數(shù)據(jù)的讀取方法酗昼,比如這么一段
const void * __log_binInfo_conv1(const void *dataOffset) {
printf("\n【conv1】層類(lèi)型為【Convolution】(卷積層)\n"
"參數(shù)配置 0=64 1=3 2=1 3=2 4=0 5=1 6=1728,即:\n"
"輸出單元 數(shù)量: 64\n"
"核 大小: 3, 3\n"
"核 膨脹: 2, 2\n"
"Pad 大小: 0, 0\n"
"是否有偏置項(xiàng): 1(是)\n"
"權(quán)重?cái)?shù)據(jù) 數(shù)量: 1728 (= 3(核高) * 3(核寬) * 3(RGB三通道) * 64(輸出單元數(shù)量)\n");
printf("\n【conv1】Load1_1: 加載weight_data數(shù)據(jù)類(lèi)型標(biāo)志(固定為自動(dòng)類(lèi)型)\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數(shù)據(jù)(1728項(xiàng)梳猪,自動(dòng)為float32類(lèi)型)\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偏置數(shù)據(jù)(64項(xiàng)麻削,固定為float32類(lèi)型)\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)打開(kāi)其下的 NcnnSrcDemo 工程
2)進(jìn)入 ViewController,解除 自定義bin文件加載測(cè)試的 注釋
/* 自定義 bin 文件加載測(cè)試 */
[self loadModel_myAnalysis];
3)運(yùn)行看看結(jié)果吧舔示,也可以用 ncnn的loadModel 去跑碟婆,然后打斷點(diǎn)看看解讀的 .bin 文件數(shù)據(jù)一致不。
了解了bin文件的信息存儲(chǔ)形式惕稻,我們當(dāng)然就可以進(jìn)行信息修改咯竖共!不同的框架模型進(jìn)行轉(zhuǎn)化時(shí),就要做這樣的事情俺祠。
哇公给,那我們可以 自己寫(xiě)轉(zhuǎn)模型的工具 啦!從技術(shù)上說(shuō)蜘渣,完全沒(méi)錯(cuò)淌铐!
(當(dāng)然我們還要補(bǔ)習(xí)神經(jīng)網(wǎng)絡(luò)中各種層的信息,以及不同框架的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì))
2.2 Detect 檢測(cè)
完成了 網(wǎng)絡(luò)初始化 load_param()蔫缸、 load_bin()之后腿准,我們可以填寫(xiě)一個(gè)輸入并使用 網(wǎng)絡(luò)提取器Extractor 計(jì)算輸出了。
2.2.1 創(chuàng)建提取器 Extractor
/* Step2.1 : 構(gòu)建并配置 提取器 */
ncnn::Extractor extractor = ncnn_net.create_extractor();
extractor.set_light_mode(true);
提取器 extractor 使用 目標(biāo)網(wǎng)絡(luò) 通過(guò) 友元函數(shù) 創(chuàng)建實(shí)例拾碌,因?yàn)樗枰@取對(duì)應(yīng)神經(jīng)網(wǎng)絡(luò)的信息吐葱;同時(shí),extractor 還可以自定義部分配置信息校翔。
Extractor含3個(gè)關(guān)鍵類(lèi)變量
1)net: 指向?qū)?yīng)網(wǎng)絡(luò)的指針
2)blob_mats: 計(jì)算的過(guò)程中存儲(chǔ)輸入弟跑、輸出的臨時(shí)數(shù)據(jù)
3)opt: 配置參數(shù)
2.2.2 extractor.input 配置輸入
/* Step2.2 : 設(shè)置輸入(將圖片轉(zhuǎn)換成ncnn::Mat結(jié)構(gòu)作為輸入) */
UIImage *srcImage = [UIImage imageNamed:@"mouth"];
ncnn::Mat mat_src;
ts_image2mat(mat_src, srcImage);
extractor.input("data", mat_src);
1)我們要 構(gòu)造一個(gè)ncnn::Mat的結(jié)構(gòu),將我們的輸入填入其中
2)利用 Extractor的input()函數(shù) 將輸入mat填入對(duì)應(yīng)的位置防症。
注意:input() 函數(shù)中的第一個(gè)字符串參數(shù)輸入的是 blob的名稱 而不是 layer的名稱 哦C霞(如有有些懵,可以回看一下 [2.1.1.1節(jié)] 的 .param的文件描述蔫敲,區(qū)分下 layer 和 blob)
2.2.3 extractor.extract 提取輸出
/* Step2.3 : 提取輸出 */
ncnn::Mat mat_dst;
extractor.extract("prob", mat_dst);
1)我們要 構(gòu)造一個(gè)ncnn::Mat的結(jié)構(gòu)饲嗽,用以承載輸出;
2)利用 Extractor的extract()函數(shù) 將計(jì)算結(jié)果填寫(xiě)到我們構(gòu)造的輸出mat中燕偶。
注意:input()函數(shù)中的第一個(gè)字符串參數(shù)輸入的是 blob的名稱 而不是 layer的名稱 哦:仍搿(如有有些懵,可以回看一下 [2.1.1.1節(jié)] 的 .param的文件描述指么,區(qū)分下 layer 和 blob)
2.2.3.1 extract() 的遞歸流程圖
ncnn 在進(jìn)行 extract() 的時(shí)候酝惧,使用了遞歸的方式,這邊將其 宏觀邏輯進(jìn)行抽象伯诬。(描繪所有的代碼細(xì)節(jié)會(huì)使圖過(guò)于復(fù)雜晚唇,不易閱讀)
2.2.3.2 extract() 最簡(jiǎn)遞歸展開(kāi)流程
之所以說(shuō)最簡(jiǎn),因?yàn)槲覀兗僭O(shè):
1 目標(biāo)網(wǎng)絡(luò)的每層都只有 一個(gè)輸入(blob) 和 一個(gè)輸出(blob)盗似,
2 使用的extract是新創(chuàng)建的(即 無(wú)緩存數(shù)據(jù))
如圖:
1)每一層進(jìn)行forward()的時(shí)候哩陕,需要一些輸入?yún)?shù),這些輸入?yún)?shù)是 由上面的層的forward()運(yùn)算輸出的赫舒。
2)只有 輸入層的輸入?yún)?shù)是我們填寫(xiě)的(2.2.2 節(jié))悍及,也正是因?yàn)樗拇嬖冢f歸得以有了終結(jié)接癌。
2.3 輸出處理
/* Step3.1 : 結(jié)果處理(獲取檢測(cè)概率最高的5種物品心赶,認(rèn)為存在) */
NSArray *rstArray = ts_mat2array(mat_dst);
NSArray *top5Array = ts_topN(rstArray, 5);
/* Step3.2 : 打印輸出 */
NSLog(@"%@", top5Array);
/* 說(shuō)明:該Demo中發(fā)現(xiàn)輸出的第一項(xiàng)是 index 為 673 的項(xiàng)目,
* 在result_info.json中查找下 "index" : "673" 發(fā)現(xiàn)對(duì)應(yīng)的描述是 鼠標(biāo)
* 也可以換其他圖片進(jìn)行檢測(cè)缺猛,但要將圖片規(guī)格化成 227 * 227 的大小才可以保證結(jié)果的準(zhǔn)確性
*/
輸出處理是根據(jù)需求具體模型需求缨叫,很靈活的。
比如我給予輸出結(jié)果的每個(gè)數(shù)字以 概念(識(shí)別到某種物品的概率) 荔燎,并對(duì)輸出結(jié)果進(jìn)行排序后取其 概率最高的 五個(gè)值耻姥。
2.4 封裝
玩一玩的話,12行代碼足夠了有咨;但若真的要工程化的話琐簇,我們還是要將 面向過(guò)程 的思路 向 面向?qū)ο?/strong> 靠攏的。
可惜的是座享,這邊只能提點(diǎn)一下 要有封裝的意識(shí)婉商。
因?yàn)樵诠绢I(lǐng)導(dǎo)決定開(kāi)源我們的SDK之前,不太方便透漏我們相關(guān)的封裝思路咯征讲。
3 文尾福利 Demo
老套路据某,文尾送福利!
這邊提供一個(gè) Ncnn 的 iOS源碼Demo诗箍。其中癣籽,你可以直接 在ncnn源碼中打斷點(diǎn),加日志滤祖,通過(guò)調(diào)試源碼的方式對(duì)ncnn快速理解筷狼。
Github地址:https://github.com/chrisYooh/ncnnSrcDemo
打開(kāi)其下的 ncnnSrcDemo工程,然后匠童,開(kāi)始愉快地 Debug 吧9〔摹:)
呃……好像最近蠻流行 文章末尾隨便塞張圖……
你猜我消消樂(lè)玩到第幾關(guān)了???