close

前些天google開源了TensorFLow Lite,並且在Android 8.1版本上為DSP、GPU、和神經網絡芯片等硬件加速器支持了神經網絡API,為了在Android移動設備上全面支持AI做足了準備。下面是根據官方的文章做了翻譯,並且加入個人的一些理解,來介紹一下何為NNAPI。另外神經網絡的介紹可參考如下鏈接

神經網絡API

注意:Neural Networks API 只能在Android 8.1 以及更高版本的系統中可用,其頭文件同樣只包含在最新的NDK 版本中。

Android Neural Networks API (NNAPI) 是一個基於Android 系統的用於可在移動設備上運行與機器學習相關的計算密集型操作的C 語言API,NNAPI 將為更高層次的可構建和訓練神經網絡的機器學習框架(如TensorFLow Lite, Caffe2, 等等)提供底層支持。這些API 將會集成到所有的Android 8.1 (以及更高版本)設備上。

NNAPI 可支持一些在Android 設備上已有的推理應用,以及開發者自定義的模型。這些推理應用例如圖像分類,預測用戶行為,和偏向性的關鍵字搜索。

AI 應用的本地化,即把AI 運算直接在移動設備上執行,不需要通過網絡與雲端交互的好處有以下幾點:

  • 低延時:你不再需要通過網絡與雲端交互,並且等待服務器響應。這對於需要成功地實時地從相機獲得連續幀的視頻應用來說是至關重要的。

  • 有效性:應用也可在網絡覆蓋不到的地方使用。

  • 快:相比只用cpu作為處理器來說,一些新的硬件加速器,如神經網絡處理器可以提供更快的運行速度。

  • 私密性:這些數據只存在於本地設備上。

  • 低成本:當所有的AI運算都在本地設備上執行時,將不再需要服務器群組。

同樣,也需要開發者折中考慮以下幾點:

  • 系統使用率: 神經網絡將會牽涉到大量的計算量,這會消耗一些電池電量。你可以在你的app中考慮監控電池狀態,由其在長時間的計算情況下。

  • 應用大小:必須注意你的模型大小。模型可能會消耗非常多的存儲空間。如果你的APK中存放了大型模型,這將會影響你的用戶體驗,你可以考慮在app安裝之後再把模型下載下來,或者使用小型模型,再或者把你的AI運算放到雲端。NNAPI是不支持雲端運行模型的。

了解神經網絡API 運行時

NNAPI 基本就是提供給機器學習庫和機器學習框架在Android 設備上調用的。App 不能直接使用NNAPI,但可通過更高級別的機器學習框架間接地使用。這些機器學習框架可通過NNAPI 使用指定的硬件加速器。

基於app 的需求和移動設備上的硬件能力,Android 的神經網絡運行時能很好地根據給定的硬件分配各自的計算量,包括專用的神經網絡硬件,圖像處理單元(GPU),以及數字信號處理器(DSP)。

在一些沒有硬件加速器的設備上,NNAPI 運行時會以優化代碼的方式把這些AI 運算放到CPU 上運行。

下圖描述了一個NNAPI 高層級的系統架構

undefined

神經網絡API 編程模型

用NNAPI 去做一些運算時,首先需要構建一個定義了具體運算的定向圖(directed graph)。這個定向圖將連接你的輸入數據(例如,從機器學習框架中傳下來的權重和偏移量),然後形成為NNAPI 運行時的模型。

NNAPI 使用了以下4 個主要的抽象概念:

  • 模型:是一個包含了數學運算和已訓練好的數據的計算圖。這些都是神經網絡相關運算,包括2維卷積,邏輯(Sigmoid)激活,修正線性激活(ReLU),等等。創建模型是一個同步操作,一旦創建成功,模型將可以跨線程和跨編譯器使用。在NNAPI中,模型可被描述為一個ANeuralNetworksModel實例。

  • 編譯器:是一個用於把NNAPI模型編譯成機器碼的配置。創建編譯器是一個同步操作,一旦創建成功,將可以跨線程和跨執行器使用。在NNAPI中,每個編譯器都可被描述為ANeuralNetworksCompilation實例。

  • 內存緩衝區:是一個共享內存,文件映射內存,或者普通的內存緩衝區。使用內存緩衝區來讓NNAPI運行時與驅動進行數據交互是很高效的。通常在app中創建一個模型同時需要創建一個包含了每個張量數組的共享內存緩衝區。除些之外,你同樣可以使用內存緩衝區去保存執行器實例所需的輸入和輸出數據。在NNAPI中,每個內存緩衝區都可被描述為一個ANeuralNetworksMemory實例。

  • 執行器:是一個把NNAPI 模型應用到輸入數據集並得出結果的接口。它是一個異步操作,多線程都能等待相同的執行器,當執行器完成工作,所有線程將會被釋放等待。在NNAPI中,每個執行器都可被描述為ANeuralNetworksExecution實例。

下圖所示的是基本的編程流程

undefined

以下是按照該編程流程一步步地實現NNAPI 模型運算,創建模型,編譯模型,和執行已經編譯的模型。

提供一組可訪問的訓練數據

已經訓練好的權重和偏置項都可存放在文件中,然後通過ANeuralNetworksMemory_createFromFd() 函數創建ANeuralNetworksMemory 實例並為NNAPI 運行時提供這些數據,在函數中傳入文件描述符即可。

// Create a memory buffer from the file that contains the trained data.
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);

在例子中雖然只用了一個ANeuralNetworksMemory 實例,其實在實際運用中可以通過多個文件來創建多個實例。

模型

模型是NNAPI計算的基本單位,一個模型是由一個或者多個操作數運算來定義的。

操作數

操作數是指在運算圖中的數據對象。包括模型的輸入輸出數據,和從一個運算流向另一運算過程中包含的中間數據。在NNAPI模型中的操作數有兩種類型:標量和張量(簡單來說就是單一常量和n維數組)

標量代表著一個單一的數字。NNAPI 支持的標量類型有32位浮點,32位整型,32位無符號整型。

大多數的NNAPI 操作數都涉及到張量,張量是一個n 維數組。NNAPI 支持的張量類型有32位整形,32位浮點,和8位量化值。

以下舉例說明一下,下圖所示為一個完整的模型,其中有兩個運算,一個是加法,隨後是乘法,其中包括了模型的輸入張量和輸出張量。

undefined

以上模型中有7個操作數(綠色圓圈代表一個操作數),這7個操作數是以加入該模型中的順序進行編號的,如第一個加入該模型的操作數被標為0 ,第二個加入模型的操作數被標為1,如此類推。

操作數是有類型的。它們在被添加到模型中時指定。一個操作數不能同時用作模型的輸入和輸出。

更多關於使用操作數的話題可參考下面更多關於操作數一節。

運算

一個運算指定了具體的運算規則,運算包括以下幾個元素:

  • 一個運算類型(如:加法,乘法,卷積)
  • 一組用於該運算的輸入數據的操作數編號
  • 一組用於存儲該運算後輸出數據的操作數編號

這一組操作數編號的順序是很重要的,更多關於運算類型可參考NNAPI API reference

在把運算加入到模型中之前必須指明該運算的輸入和輸出的操作數

加入到模型中的順序是跟模型執行運算的順序沒關係的,因為NNAPI可以根據這個運算圖來決定操作的執行順序。

NNAPI可支持的運算總結到如下表,具體的運算說明可參考鏈接

Category Operations
Element-wise mathematical operations ANEURALNETWORKS_ADD 
ANEURALNETWORKS_MUL 
ANEURALNETWORKS_FLOOR
Array operations ANEURALNETWORKS_CONCATENATION 
ANEURALNETWORKS_DEPTH_TO_SPACE 
ANEURALNETWORKS_DEQUANTIZE 
ANEURALNETWOKRS_RESHAPE 
ANEURALNETWORKS_SPACE_TO_DEPTH
Image operations ANEURALNETWORKS_RESIZE_BILINEAR
Lookup operations ANEURALNETWORKS_HASHTABLE_LOOKUP 
ANEURALNETWORKS_EMBEDDING_LOOKUP
Normalization operations ANEURALNETWORKS_L2_NORMALIZATION 
ANEURALNETWORKS_LOCAL_RESPONSE_NORMALIZATION
Convolution operations ANEURALNETWORKS_CONV_2D 
ANEURALNETWORKS_DEPTHWISE_CONV_2D
Pooling operations ANEURALNETWORKS_AVERAGE_POOL_2D 
ANEURALNETWORKS_L2_POOL_2D 
ANEURALNETWORKS_MAX_POOL_2D
Pooling operations ANEURALNETWORKS_AVERAGE_POOL_2D 
ANEURALNETWORKS_L2_POOL_2D 
ANEURALNETWORKS_MAX_POOL_2D
Activation operations ANEURALNETWORKS_LOGISTIC 
ANEURALNETWORKS_RELU 
ANEURALNETWORKS_RELU1 
ANUERALNETWORKS_RELU6 
ANEURALNETOWORKS_SOFTMAX 
ANEURALNETWORKS_TANH
Other operations ANEURALNETWORKS_FULLY_CONNECTED 
ANEURALNETWORKS_LSH_PROJECTION 
ANEURALNETWORKS_LSTM 
ANEURALNETWORKS_RNN 
ANEURALNETWORKS_SVDF

構建模型

以下是構建一個模型的步驟

1. 調用ANeuralNetworksModel_create()函數構建一個空的模型
    以上圖所示的模型為例,構建一個NNAPI模型。

ANeuralNetworksModel* model = NULL;
ANeuralNetworksModel_create(&model);

2. 調用ANeuralNetworks_addOperand()函數在已經構建的模型中增加操作數,操作數的數據類型可參考ANeuralNetworksOperandType

// In our example, all our tensors are matrices of dimension [3, 4].
ANeuralNetworksOperandType tensor3x4Type;
tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
tensor3x4Type.scale = 0.f;    // These fields are useful for quantized tensors.
tensor3x4Type.zeroPoint = 0;  // These fields are useful for quantized tensors.
tensor3x4Type.dimensionCount = 2;
uint32_t dims[2] = {3, 4};
tensor3x4Type.dimensions = dims;

// We also specify operands that are activation function specifiers.
ANeuralNetworksOperandType activationType;
activationType.type = ANEURALNETWORKS_INT32;
activationType.scale = 0.f;
activationType.zeroPoint = 0;
activationType.dimensionCount = 0;
activationType.dimensions = NULL;

// Now we add the seven operands, in the same order defined in the diagram.
ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 0
ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 1
ANeuralNetworksModel_addOperand(model, &activationType); // operand 2
ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 3
ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 4
ANeuralNetworksModel_addOperand(model, &activationType); // operand 5
ANeuralNetworksModel_addOperand(model, &tensor3x4Type);  // operand 6

3. 調用ANeuralNetworks_setOperandValue()和ANeuralNetworks_setOperandValuesFromMemory()函數把已經訓練好的權重和偏置項加載到操作數中。
    在下面的代碼中,mem1為上面使用內存緩衝區裝載的訓練好數據。

// In our example, operands 1 and 3 are constant tensors whose value was
// established during the training process.
const int sizeOfTensor = 3 * 4 * 4;    // The formula for size calculation is dim0 * dim1 * elementSize.
ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);

// We set the values of the activation operands, in our example operands 2 and 5.
int32_t noneValue = ANEURALNETWORKS_FUSED_NONE;
ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue));
ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));

4. 調用ANeuralNetworks_addOperation()函數在模型中增加運算
    調用該函數時需要傳入以下參數

  • 運算類型
  • 輸入操作數的個數
  • 一個存放了輸入操作數編號的數組
  • 輸出操作數的個數
  • 一個存放了輸出操作數編號的數組

請注意,一個操作數不能既作為輸入又作為輸出。

// We have two operations in our example.
// The first consumes operands 1, 0, 2, and produces operand 4.
uint32_t addInputIndexes[3] = {1, 0, 2};
uint32_t addOutputIndexes[1] = {4};
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes); 

// The second consumes operands 3, 4, 5, and produces operand 6.
uint32_t multInputIndexes[3] = {3, 4, 5};
uint32_t multOutputIndexes[1] = {6};
ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);

5. 調用ANeuralNetworksModel_identifyInputsAndOutputs() 函數指定哪些操作數是整個模型的輸入和輸出。

// Our model has one input (0) and one output (6).
uint32_t modelInputIndexes[1] = {0};
uint32_t modelOutputIndexes[1] = {6};
ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);

6. 調用ANeuralNetworksModel_finish() 函數來結束模型的構建,如果成功構建將返回ANEURALNETWORKS_NO_ERROR 值。

ANeuralNetworksModel_finish(model);

編譯

對模型進行編譯是取決於你的模型運行於哪種硬件加速器上,當一個模型構建完後,需要為其進行編譯才能真正跑在硬件加速器上,最終會生成對應硬件加速器的機器碼,並告知硬件加速器的驅動將要執行該模型。

編譯模型需要以下幾個步驟:

1. 調用ANeuralNetworksCompilation_create()函數創建新的編譯器實例

// Compile the model.
ANeuralNetworksCompilation* compilation;
ANeuralNetworksCompilation_create(model, &compilation);

2. 可以調用ANeuralNetworksCompilation_setPreference() 函數來支配運行時的功耗和性能

// Ask to optimize for low power consumption.
ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
  • ANEURALNETWORKS_PREFER_LOW_POWER: 節能,往往運用於在長時間的執行情況下。
  • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER:盡可能以最快速度執行,不管電池電量的消耗。
  • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED:以最大化吞吐量執行,比如處理相機的連續幀的情況下。

3. 最後調用ANeuralNetworksCompilation_finish()函數結束編譯器定義,成功將返回ANEURALNETWORKS_NO_ERROR。

ANeuralNetworksCompilation_finish(compilation);

執行

在執行模型時,需要給定輸入數據集合,以及用於存放輸出數據的內存緩衝區,這些都需要app去創建。

以下是執行的步驟:

1. 調用ANeuralNetworksExecution_create() 創建新的執行器實例。

// Run the compiled model against a set of inputs.
ANeuralNetworksExecution* run1 = NULL;
ANeuralNetworksExecution_create(compilation, &run1);

2. 調用ANeuralNetworksExecution_setInput() 或者ANeuralNetworksExecution_setInputFromMemory() 函數來設置輸入數據集合

重要:在你調用ANeuralNetworksModel_identifyInputsAndOutputs()函數為模型指定輸入和輸出的操作數時已經指定了哪些操作數編號是輸入,哪些操作數編號是輸出,在為這些輸入操作數設置數據時不要混淆了這些編號。

// Set the single input to our sample model. Since it is small, we won’t use a memory buffer.
float32 myInput[3, 4] = { ..the data.. };
ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));

3. 調用ANeuralNetworksExecution_setOutput()或者ANeuralNetworksExecution_setOutputFromMemory()函數來指定模型輸出的數據集合存入的內存緩衝區

// Set the output.
float32 myOutput[3, 4];
ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));

4. 調用ANeuralNetworksExecution_startCompute() 函數啟動執行,成功則返回ANEURALNETWORKS_NO_ERROR。

// Starts the work. The work proceeds asynchronously.
ANeuralNetworksEvent* run1_end = NULL;
ANeuralNetworksExecution_startCompute(run1, &run1_end);

5. 調用ANeuralNetworksEvent_wait() 函數等待硬件執行結束,成功則返回ANEURALNETWORKS_NO_ERROR,前面提到了,執行操作是異步操作,所以多線程可同時調用該函數等待執行結束。

// For our example, we have no other work to do and will just wait for the completion.
ANeuralNetworksEvent_wait(run1_end);
ANeuralNetworksEvent_free(run1_end);
ANeuralNetworksExecution_free(run1);

6. 前面提到的,當一個模型創建完成後,可供不同線程使用該編譯好的模型,只需要給定不同的輸入數據集合以及創建新的ANeuralNetworksExecution實例。

// Apply the compiled model to a different set of inputs.
ANeuralNetworksExecution* run2;
ANeuralNetworksExecution_create(compilation, &run2);
ANeuralNetworksExecution_setInput(run2, ...);
ANeuralNetworksExecution_setOutput(run2, ...);
ANeuralNetworksEvent* run2_end = NULL;
ANeuralNetworksExecution_startCompute(run2, &run2_end);
ANeuralNetworksEvent_wait(run2_end);
ANeuralNetworksEvent_free(run2_end);
ANeuralNetworksExecution_free(run2);

清除

釋放所有用於模型運算的資源,如下:

 // Cleanup
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
ANeuralNetworksMemory_free(mem1);

更多關於操作數

以下章節是關於操作數的更高級應用。

Quantized tensors

這個名詞不清楚其中文的專業術語是什麼,量化數組?所以只能用英文來描述,它是一個描述n 維浮點形數組的簡單描述方式。

NNAPI支持8位非對稱量化數組,裡面每一個元素都是8位整數,其實代表的是8位浮點數,與這些量化數組相關的有刻度(scale)和零點值(zero point value),這些是用來把8位整數變換為8位浮點數。
其公式如下:

(cellValue - zeroPoint) * scale

zeroPoint value 是一個32位整形數,也是一個刻度32 位浮點數的值。

對比32 位浮點張量(其實就是32位浮點形的數組),8 位quantized tensors(其實就是8 位整形的數組)有以下幾點優勢:

  • 你的應用佔用的空間會更小,用作表示已經訓練好的權重的話,可以節約四分之3的存儲空間。
  • 運算更快,由於從內存中取出更少的數據和這些數據在處理器執行更有效率,如DSP 更適合處理整形數。

雖然可以將浮點模型轉換為量化模型,但我們的經驗表明,通過直接訓練量化模型(quantized model)可以獲得更好的結果。實際上,神經網絡會補償每個值的增加粒度。針對每一個quantized tensors,刻度(scale)和零點值(zero point value)是在訓練過程中確定。

在NNAPI中,可以通過在定義操作數數據類型時指定ANeuralNetworksOperandType結構體中的type變量為ANEURALNETWORKS_TENSOR_QUANT8_ASYMM來定義量化數組類型,同時也必須設置刻度(scale)和零點值(zeroPoint)。

 

 

 

 

 

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 阿吉吉 的頭像
    阿吉吉

    小吉

    阿吉吉 發表在 痞客邦 留言(0) 人氣()