1. 程式人生 > >ARM+Movidius VPU 目標識別除錯筆記(二)

ARM+Movidius VPU 目標識別除錯筆記(二)

演算法載入

ARM+Movidius VPU 目標識別除錯筆記(一)一文中,我們通過對Ncsdk的分析,已經成功搭建了其開發環境,並且能成功執行簡單的HelloWorld程式了。 那麼我們下一步工作就是要分析清楚Ncsdk是如果操作來實現演算法加速的。 首先我們還是根據官方demo來分析一下。

獲取App Zoo

首先我們從github上獲取Ncsdk App Zoo,這裡面有很多Caffe和tensorflow的demo app,github專案連結在此。原始碼下載命令為:

git clone -b ncsdk2 https://github.com/movidius/ncappzoo.git

App Zoo 原始碼獲取之後,我們看到如下圖圖一所示的目錄結構:

Zoo目錄結構 我們梳理一下上圖中幾個目錄 apps: 一些demo程式的原始碼 caffe/tensorflow: 這兩個目錄分別存放基於caffe和tensorflow的計算圖 data:執行demo的一些圖形庫

我們可以直接在apps路徑中找C/C++的官方demo,找遍這二十幾個例程,只有hello_ncs_cpp和multistick_cpp唯二的例程使用C/C++實現的。hello_ncs_cpp前面已經說過,那麼只剩下multistick_cpp可以供我們學習學習C/C++下的api呼叫方法。那麼我們直接來 multistick.cpp 這個檔案來分析演算法預測過程。

演算法預測

這個例程使用兩個VPU裝置來運行了兩張深度學習的計算圖,第一張是googlenet.graph,第二張是squeezenet.graph,分別使用這兩個網路來對nps_electric_guitar.png和nps_baseball.png兩張圖片進行image classifier 分類。 GoogLeNet和SqueezeNet兩個深度學習網路的演算法模型都在圖一所示的caffe資料夾下面,我們可以看到這兩張網路計算圖graph都是通過Makefile編譯出來的,但是由於我使用的是tensorflow框架,對caffe沒有過多的瞭解,所以這裡並沒有深入Makefile的編譯過程,此處不做延伸。 另外,在每個檔案加中都有一個run.py的python3指令碼,它是用來校驗和測試這兩個網路的效能,執行完成之後,具體的效能引數對比如下表一

所述:

Net Name GoogLeNet SqueezeNet
SHAVE Number 12 12
Inference time 94.02 ms 46.51 ms
Bandwidth 579.31 MB/sec 701.56 MB/sec
Input Tensor [224, 224, 3] [227, 227, 3]
Layer Number 22 10

從上表中可以看出來,GoogLeNet網路預測的時間為94ms,SqueezeNet網路預測的時間為64ms;GoogLeNet輸入層張量的維度為[224, 224, 3],而SqueezeNet的輸入層張量為[227, 227, 3]

下面來看看程式碼實現演算法預測的過程吧,如下:

// 第一步,開啟裝置
if (!OpenOneNCS(0, &devHandle1))
{
    // couldn't open first NCS device
    // TODO
}

// 第二步,載入計算圖
if (!LoadGraphToNCS(devHandle1, GOOGLENET_GRAPH_FILE_NAME, &graphHandleGoogleNet))
{
    // TODO
}

// 第三步,獲取輸入輸出張量描述符
// Read tensor descriptors for Googlenet
struct ncTensorDescriptor_t inputTdGooglenet;
struct ncTensorDescriptor_t outputTdGooglenet;
length = sizeof(struct ncTensorDescriptor_t);
ncGraphGetOption(graphHandleGoogleNet, NC_RO_GRAPH_INPUT_TENSOR_DESCRIPTORS, &inputTdGooglenet,  &length);
ncGraphGetOption(graphHandleGoogleNet, NC_RO_GRAPH_OUTPUT_TENSOR_DESCRIPTORS, &outputTdGooglenet,  &length);

// 第四步,建立讀寫FIFO
// Init & Create Fifos for Googlenet
struct ncFifoHandle_t * bufferInGooglenet;
struct ncFifoHandle_t * bufferOutGooglenet;
rc = ncFifoCreate("fifoIn0", NC_FIFO_HOST_WO, &bufferInGooglenet);
rc += ncFifoAllocate(bufferInGooglenet, devHandle1, &inputTdGooglenet, 2);
rc += ncFifoCreate("fifoOut0", NC_FIFO_HOST_RO, &bufferOutGooglenet);
rc += ncFifoAllocate(bufferOutGooglenet, devHandle1, &outputTdGooglenet, 2);
if(rc)
    printf("Fifo allocation failed for Googlenet, rc=%d\n", rc);

// 第五步,預測演算法並獲取結果
printf("\n--- NCS 1 inference ---\n");
DoInferenceOnImageFile(graphHandleGoogleNet, devHandle1, bufferInGooglenet, bufferOutGooglenet, GOOGLENET_IMAGE_FILE_NAME, networkDimGoogleNet, networkMeanGoogleNet);
printf("-----------------------\n");

// 第六步,釋放資源
retCode = ncFifoDestroy(&bufferInGooglenet);
retCode = ncFifoDestroy(&bufferOutGooglenet);
retCode = ncGraphDestroy(&graphHandleGoogleNet);
graphHandleGoogleNet = NULL;

retCode = ncDeviceClose(devHandle1);
devHandle1 = NULL;
  1. 建立和開啟NCS控制代碼,開啟該控制代碼後我們就能獲取裝置的usb_device,以便操作usb來進行計算圖、輸入層張量和預測結果的讀寫。
  2. 載入計算圖,通過呼叫 LoadGraphToNCS 函式,程式將建立計算圖控制代碼並將計算圖載入到VPU中,其實這裡的計算圖不光包括演算法的計算圖,還包括了模型訓練好的所有引數、權重、偏置等。
  3. 獲取輸入層和輸出層的張量描述符。
  4. 建立了兩個buffer控制代碼,一個bufferInGooglenet用來輸入影象張量,另一個bufferOutGooglenet用來儲存網路計算後的結果。
  5. 呼叫介面 DoInferenceOnImageFile 預測網路並獲取網路輸出層的張量。
  6. 釋放控制代碼,回收使用的記憶體資源。

這部分程式碼的核心流程在 DoInferenceOnImageFile 函式中實現,接下來我們來看看 DoInferenceOnImageFile 函式的處理流程

bool DoInferenceOnImageFile(struct ncGraphHandle_t *graphHandle, 
    struct ncDeviceHandle_t *dev, struct ncFifoHandle_t *bufferIn, 
    struct ncFifoHandle_t *bufferOut, const char* imageFileName, 
    int networkDim, float* networkMean)
{
    ncStatus_t retCode;
    struct ncTensorDescriptor_t td;
    struct ncTensorDescriptor_t resultDesc;
    unsigned int length;

    // 第一步,載入並下采樣輸入圖片
    float* imageBuf = LoadImage(imageFileName, networkDim, networkMean);

    // calculate the length of the buffer that contains the floats. 
    unsigned int lenBuf = 3*networkDim*networkDim*sizeof(*imageBuf);

    // Read descriptor for input tensor
    length = sizeof(struct ncTensorDescriptor_t);
    retCode = ncFifoGetOption(bufferIn, NC_RO_FIFO_GRAPH_TENSOR_DESCRIPTOR, &td, &length);

    // 第二步,將下采樣後的影象資料寫入到FIFO中
    // Write tensor to input fifo
    retCode = ncFifoWriteElem(bufferIn, imageBuf, &lenBuf, NULL);

    // 第三步,預測網路並獲取輸出層FIFO控制代碼
    // Start inference
    retCode = ncGraphQueueInference(graphHandle, &bufferIn, 1, &bufferOut, 1);
    free(imageBuf);

    unsigned int outputDataLength;
    length = sizeof(unsigned int);
    retCode = ncFifoGetOption(bufferOut, NC_RO_FIFO_ELEMENT_DATA_SIZE, &outputDataLength, &length);
    float* resultData = (float*) malloc(outputDataLength);

    void* userParam;
    // 第四步,從輸出層控制代碼中讀取預測結果
    retCode = ncFifoReadElem(bufferOut, (void*) resultData, &outputDataLength, &userParam);
    printf("Successfully got the inference result for image %s\n", imageFileName);

    // 第六步,列印結果
    unsigned int numResults = outputDataLength/sizeof(float);
    float maxResult = 0.0;
    int maxIndex    = -1;
    for (int index = 0; index < numResults; index++)
    {
        if (resultData[index] > maxResult)
        {
            maxResult = resultData[index];
            maxIndex = index;
        }
    }
    printf("Index of top result is: %d\n", maxIndex);
    printf("Probability of top result is: %f\n", resultData[maxIndex]);
    free(resultData);
}

從上述程式碼來看,邏輯也很清晰明瞭,操作都是傻瓜式的,且很容易理解。需要注意幾點: 1. 輸入層張量維度:對於這兩個網路來說,輸入層張量的維度前面已經介紹過了,GoogLeNet為[224, 224, 3],SqueezeNet為[227, 227, 3],我們在講資料寫入FIFO前必須要保證圖片下采樣到維度相同,否則會報錯。 2. 輸出層張量維度:對於分類問題,輸出層都是網路計算完成之後得到的各個類別對應的分值,我們認為分值最大的那個類別就是預測出來的類別,caffe的這兩個網路訓練了1000個類別的影象集,所以輸出張量維度為[1000, 1]。我們可以通過檢視 data\ilsvrc12\synset_words.txt 檢視所有的類別列表以便檢測預測結果是否準確。 從上面的分析我們已經明確瞭如何使用Movidius來開發一個神經網路的應用程式了,下面回到我們的主題,關於目標識別功能的開發。

目標識別

目標識別(objects detection)是指從一幅場景(圖片)中找出目標,包括檢測(where)和識別(what)兩個過程。
它分為目標分割、目標檢測和目標識別三個過程。
它的更深層次的演算法是目標定位和跟蹤。

網路模型

計算機視覺和深度學習發展到如今這個階段,業界已經有了很多效果不錯的目標識別演算法模型,所以tensorflow 1.4.0版本以後加入了目標識別的model,這個model裡面包含了24種目標識別的網路型別,詳細資訊可以閱讀 detection_models_zoo。這裡列出了這24種網路在Nvidia GeForce GTX TITAN X顯示卡上面執行的時間和在COCO資料集上面測試的mAP。同時,tensorflow把這些網路模型增加到框架裡面之後,可以及其方便地通過呼叫已經訓練好的這些網路模型來實現目標識別的演算法。

既然有如此多現成的網路模型,我決定使用現成的模型來測試一下效果。由於最終希望演算法實時性足夠高,而Movidius的計算能力肯定遠遠不能和Nvidia GeForce GTX TITAN X相提並論,所以我計劃使用ssdlite_mobilenet_v2_coco來驗證效果。官方測試的網路速率為27ms,COCO資料集上的mAP為22,速率優先,所以我們只能選擇適當地犧牲效果。

生成計算圖

既然網路模型選定了,我們下一步就是需要編譯生成網路的計算圖來按前面分析的步驟一步一步地把功能實現。從最後的結果來看,官方的生成的模型是沒辦法直接被Ncsdk的編譯工具相容的,因此如果需要去改動模型或者是Ncsdk的編譯工具的話,改動量是很大的,所以目前為止我沒能把ssdlite_mobilenet_v2的計算圖編譯出來。 同時,我另外寫了一篇博文作為這個移植過程中的踩坑記。 基於上述的結論,我也只能無奈地選擇重新選擇網路模型,並且這麼看來tensorflow 提供的其他目標識別的模型也是沒辦法直接移植過來的,都需要去修改網路模型的原始碼,這個是我之前沒預料到的情況。 最終,我使用的是Tiny YOLO v2的網路模型來實現移植。關於Tiny YOLO V2這個網路模型在網上可以查到很多相關的介紹說明,可以實現20個類別的目標識別,生成的VPU上的計算圖有31.5MB,大小可以滿足我們的專案需求。 關於YOLO的介紹和說明,我們可以檢視darknet網站,其實YOLO V2版本已經比較老了,今年YOLO V3的版本已經發布了。而Tiny YOLO比YOLO的精確度會低一些,但是由於是在VPU上面執行網路,儘量選用了耗時比較小的網路。

下面分別介紹一下Tiny YOLO V2網路的輸入層和輸出層 輸入層:[416, 416, 3]維張量,416 x 416 是輸入圖片大小,輸入影象中的每個畫素值需要除255進行歸一化。第三維是色彩分量通道,按R、G、B的順序進行儲存。 輸出層:[13, 13, 5, 25]維張量,整張圖片劃分了13 x 13個網格,每個網格有5個anchor box,每個box有25個取值,分別為四個座標值、1個當前box是否含檢測物件的置信度、預測出的20個類別對應的分值。 預測結果:取分數和置信度的乘積大於0.4的box,然後對這些boxes進行非極大值抑制(NMS, non-maximum suppression)後得到最終的檢測box。

到此,我們就把整個功能已經梳理通了,後面就是編碼實現這部分演算法功能。後面第三部分我再針對專案具體實施部分做一些梳理和描述。

參考連結