直接內建NCNN的缺點
直接內建NCNN相對麻煩,為SqueezeNet接入NCNN,把相關的模型檔案,NCNN的頭檔案和庫,JNI調用,前處理和後處理相關業務邏輯等。把這些内容都放在SqueezeNet Sample工程裡。這樣簡單直接的內建方法,問題也很明顯,和業務耦合比較多,不具有通用性,前處理後處理都和SqueezeNcnn這個Sample有關,不能很友善地提供給其他業務元件使用。深入思考一下,如果我們把AI業務,作為一個一個單獨的AI元件提供給業務的同學使用,會發生這樣的情況:

每個元件都要依賴和包含NCNN的庫,而且每個元件的開發同學,都要去熟悉NCNN的接口,寫C的調用代碼,寫JNI。是以我們很自然地會想到要提取一個NCNN的元件出來,提取以後如圖所示:
AOE SDK裡的NCNN元件
有了AOE SDK,就可以使操作更加簡便。在AOE開源SDK裡,我們提供了NCNN元件,下面我們從4個方面來講一講NCNN元件:
1.NCNN元件的設計
2.對SqueezeNet Sample的改造
3.應用如何接入NCNN元件
4.對NCNN元件的一些思考
NCNN元件的設計
NCNN元件的設計理念是元件裡不包含具體的業務邏輯,隻包含對NCNN接口的封裝和調用。具體的業務邏輯,由業務方在外部實作。在接口定義和設計上,我們參考了TF Lite的源碼和接口設計。目前提供的對外調用接口,如下圖:
// 加載模型和param
void loadModelAndParam(...)
// 初始化是否成功
boolean isLoadModelSuccess()
// 輸入rgba資料
void inputRgba(...)
// 進行推理
void run(...)
// 多輸入多輸出推理
void runForMultipleInputsOutputs(...)
// 得到推理結果
Tensor getOutputTensor(...)
// 關閉和清理記憶體
void close()
而機智騷年本人,用的是這個:
├── AndroidManifest.xml
├── cpp
│ └── ncnn
│ ├── c_api_internal.h
│ ├── include
│ ├── interpreter.cpp
│ ├── Interpreter.h
│ ├── jni_util.cpp
│ ├── jni_utils.h
│ ├── nativeinterpreterwrapper_jni.cpp
│ ├── nativeinterpreterwrapper_jni.h
│ ├── tensor_jni.cpp
│ └── tensor_jni.h
├── java
│ └── com
│ └── didi
│ └── aoe
│ └── runtime
│ └── ncnn
│ ├── Interpreter.java
│ ├── NativeInterpreterWrapper.java
│ └── Tensor.java
└── jniLibs
├── arm64-v8a
│ └── libncnn.a
└── armeabi-v7a
└── libncnn.a
●Interpreter,提供給外部調用,提供模型加載,推理這些方法。
●NativeInterpreterWrapper是具體的實作類,裡面對native進行調用。
●Tensor,主要是一些資料和native層的互動。
AOE NCNN用的好,任務完成早,奧秘在此。
1.支援多輸入多輸出。
2.使用ByteBuffer來提升效率。
3.使用Object作為輸入和輸出(實際支援了Bytebuffer和多元資料組)
光說不練假把式,AOE NCNN的實作過程,且聽我細細道來。
如何支援多輸入多輸出
為了支援多輸入和多輸出,我們在Native層建立了一個Tensor對象的清單,每個Tensor對象裡儲存了相關的輸入和輸出資料。Native層的Tensor對象,通過tensor_jni提供給java層調用,java層維護這個指向native層tensor的“指針”位址。這樣在有多輸入和多輸出的時候,隻要拿到這個清單裡的對應的Tensor,就可以就行資料的操作了。
ByteBuffer的使用
ByteBuffer,位元組緩存區處理子節的,比傳統的數組的效率要高。
DirectByteBuffer,使用的是堆外記憶體,省去了資料到核心的拷貝,是以效率比用ByteBuffer要高。
當然ByteBuffer的使用方法不是我們要說的重點,我們說說使用了ByteBuffer以後,給我們帶來的好處:
1. 接口裡的位元組操作更加便捷,例如裡面的putInt,getInt,putFloat,getFloat,flip等一系列接口,可以很友善的對資料進行操作。
2. 和native層做互動,使用DirectByteBuffer,提升了效率。我們可以簡單了解為java層和native層可以直接對一塊“共享”記憶體進行操作,減少了中間的位元組的拷貝過程。
如何使用Object作為輸入和輸出
目前我們隻支援了ByteBuffer和MultiDimensionalArray。在實際的操作過程中,如果是ByteBuffer,我們會判斷是否是direct buffer,來進行不同的讀寫操作。如果是MultiDimensionalArray,我們會根據不同的資料類型(例如int, float等),次元等,來對資料進行讀寫操作。
對SqueezeNet Sample的改造
內建AOE NCNN元件以後,讓SqueezeNet依賴NCNN Module,SqueezeNet Sample裡面隻包含了模型檔案,前處理和後處理相關的業務邏輯,前處理和後處理可以用java,也可以用c來實作,由具體的業務實作來決定。新的代碼結構變得非常簡潔,目錄如下:
├── AndroidManifest.xml
├── assets
│ └── squeeze
│ ├── model.config
│ ├── squeezenet_v1.1.bin
│ ├── squeezenet_v1.1.id.h
│ ├── squeezenet_v1.1.param.bin
│ └── synset_words.txt
└── java
└── com
└── didi
└── aoe
└── features
└── squeeze
└── SqueezeInterpreter.java
↑ Sample也适用于其他的AI業務元件對NCNN元件的調用。
應用如何接入NCNN元件
對NCNN元件的接入,有兩種方式
●直接接入
●通過AOE SDK接入
▲兩種接入方式比較:
不BATTLE了,我單方面宣布,AOE SDK完勝!
對NCNN元件的總結和思考
通過對NCNN元件的封裝,現在業務內建NCNN更加快捷友善了。之前我們一個新的業務內建NCNN,可能需要半天到一天的時間。使用AOE NCNN元件以後,可能隻需要1-2小時的時間。當然NCNN元件目前還存在很多不完善的地方,我們對NCNN還需要去加深學習和了解。後面會通過不斷的學習,持續的對NCNN元件進行改造和優化。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - A o E - - - - - - - - - - - - - - - - - - - - - - - - - - - -