12行代碼完成AI物體識(shí)別 - ncnn前向計(jì)算流程淺析

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 加載模型

ncnniOS 端使用 .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)重衔峰、偏置信息等)

ncnn官方的Demo中的模型文件.png

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)容格式

squeezenet_v1.1.param 部分信息

一個(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ō)明侍匙。

層描述.png

如圖,每一行層描述的內(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)中有 blobslayers 兩個(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層

卷積層舉例.png

.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)

用vim打開(kāi).bin文件.png

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文件信息讀取.png

了解了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ū)分下 layerblob

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ū)分下 layerblob

2.2.3.1 extract() 的遞歸流程圖

ncnn 在進(jìn)行 extract() 的時(shí)候酝惧,使用了遞歸的方式,這邊將其 宏觀邏輯進(jìn)行抽象伯诬。(描繪所有的代碼細(xì)節(jié)會(huì)使圖過(guò)于復(fù)雜晚唇,不易閱讀)

遞歸實(shí)現(xiàn).png

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ù))

extract()遞推展開(kāi).png

如圖:
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è) NcnniOS源碼Demo诗箍。其中癣籽,你可以直接 在ncnn源碼中打斷點(diǎn),加日志滤祖,通過(guò)調(diào)試源碼的方式對(duì)ncnn快速理解筷狼。

Github地址:https://github.com/chrisYooh/ncnnSrcDemo
打開(kāi)其下的 ncnnSrcDemo工程,然后匠童,開(kāi)始愉快地 Debug 吧9〔摹:)

隨性的圖.png

呃……好像最近蠻流行 文章末尾隨便塞張圖……
你猜我消消樂(lè)玩到第幾關(guān)了???

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汤求,一起剝皮案震驚了整個(gè)濱河市俏险,隨后出現(xiàn)的幾起案子严拒,更是在濱河造成了極大的恐慌,老刑警劉巖竖独,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裤唠,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡莹痢,警方通過(guò)查閱死者的電腦和手機(jī)种蘸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)竞膳,“玉大人航瞭,你說(shuō)我怎么就攤上這事√贡伲” “怎么了刊侯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)长窄。 經(jīng)常有香客問(wèn)我滔吠,道長(zhǎng),這世上最難降的妖魔是什么挠日? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任疮绷,我火速辦了婚禮,結(jié)果婚禮上嚣潜,老公的妹妹穿的比我還像新娘冬骚。我一直安慰自己,他們只是感情好懂算,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布只冻。 她就那樣靜靜地躺著,像睡著了一般计技。 火紅的嫁衣襯著肌膚如雪喜德。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天垮媒,我揣著相機(jī)與錄音舍悯,去河邊找鬼。 笑死睡雇,一個(gè)胖子當(dāng)著我的面吹牛萌衬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播它抱,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼秕豫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了观蓄?” 一聲冷哼從身側(cè)響起混移,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤祠墅,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后沫屡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體饵隙,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撮珠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年沮脖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芯急。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡勺届,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出娶耍,到底是詐尸還是另有隱情免姿,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布榕酒,位于F島的核電站胚膊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏想鹰。R本人自食惡果不足惜紊婉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辑舷。 院中可真熱鬧喻犁,春花似錦、人聲如沸何缓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)碌廓。三九已至传轰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谷婆,已是汗流浹背慨蛙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留波材,地道東北人股淡。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像廷区,于是被迫代替她去往敵國(guó)和親唯灵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容