第十七章_模型壓縮、加速及移動端部署

文章目錄

第十七章 模型壓縮及移動端部署

Markdown Revision 1;
Date: 2018/11/4
Editor: 談繼勇
Contact: [email protected]
updata:貴州大學碩士張達峯

17.1 爲什麼需要模型壓縮和加速?

(1)隨着AI技術的飛速發展,越來越多的公司希望在自己的移動端產品中注入AI能力

(2)對於在線學習和增量學習等實時應用而言,如何減少含有大量層級及結點的大型神經網絡所需要的內存和計算量顯得極爲重要。
(3)智能設備的流行提供了內存、CPU、能耗和寬帶等資源,使得深度學習模型部署在智能移動設備上變得可行。
(4)高效的深度學習方法可以有效的幫助嵌入式設備、分佈式系統完成複雜工作,在移動端部署深度學習有很重要的意義。

17.2 目前有哪些深度學習模型壓縮方法?

https://blog.csdn.net/wspba/article/details/75671573
https://blog.csdn.net/Touch_Dream/article/details/78441332

17.2.1 前端壓縮

(1)知識蒸餾(簡單介紹)
一個複雜的模型可以認爲是由多個簡單模型或者強約束條件訓練而來,具有很好的性能,但是參數量很大,計算效率低,而小模型計算效率高,但是其性能較差。知識蒸餾是讓複雜模型學習到的知識遷移到小模型當中,使其保持其快速的計算速度前提下,同時擁有複雜模型的性能,達到模型壓縮的目的。但與剪枝、量化等方法想比,效果較差。(https://blog.csdn.net/Lucifer_zzq/article/details/79489248)
(2)緊湊的模型結構設計(簡單介紹)
緊湊的模型結構設計主要是對神經網絡卷積的方式進行改進,比如使用兩個3x3的卷積替換一個5x5的卷積、使用深度可分離卷積等等方式降低計算參數量。
(3)濾波器層面的剪枝(簡單介紹)
參考鏈接 https://blog.csdn.net/JNingWei/article/details/79218745 補充優缺點
濾波器層面的剪枝屬於非結構花剪枝,主要是對較小的權重矩陣整個剔除,然後對整個神經網絡進行微調。此方式由於剪枝過於粗放,容易導致精度損失較大,而且部分權重矩陣中會存留一些較小的權重造成冗餘,剪枝不徹底。

17.2.2 後端壓縮

(1)低秩近似 (簡單介紹,參考鏈接補充優缺點)
在卷積神經網絡中,卷積運算都是以矩陣相乘的方式進行。對於複雜網絡,權重矩陣往往非常大,非常消耗存儲和計算資源。低秩近似就是用若干個低秩矩陣組合重構大的權重矩陣,以此降低存儲和計算資源消耗。
優點:

  • 可以降低存儲和計算消耗;

  • 一般可以壓縮2-3倍;精度幾乎沒有損失;

缺點:

  • 模型越複雜,權重矩陣越大,利用低秩近似重構參數矩陣不能保證模型的性能

(2)未加限制的剪枝 (簡單介紹,參考鏈接補充優缺點)
剪枝操作包括:非結構化剪枝和結構化剪枝。非結構化剪枝是對神經網絡中權重較小的權重或者權重矩陣進剔除,然後對整個神經網絡進行微調;結構化剪枝是在網絡優化目標中加入權重稀疏正則項,使部分權重在訓練時趨於0。

優點:

  • 保持模型性能不損失的情況下,減少參數量9-11倍;

  • 剔除不重要的權重,可以加快計算速度,同時也可以提高模型的泛化能力;

缺點:

  • 非結構化剪枝會增加內存訪問成本;

  • 極度依賴專門的運行庫和特殊的運行平臺,不具有通用性;

  • 壓縮率過大時,破壞性能;

(3)參數量化 (簡單介紹,參考鏈接補充優缺點)
神經網絡的參數類型一般是32位浮點型,使用較小的精度代替32位所表示的精度。或者是將多個權重映射到同一數值,權重共享
優點:

  • 模型性能損失很小,大小減少8-16倍;

缺點:

  • 壓縮率大時,性能顯著下降;

  • 依賴專門的運行庫,通用性較差;

(4)二值網絡 (簡單介紹,參考鏈接補充優缺點)
對於32bit浮點型數用1bit二進制數-1或者1表示。
優點:

  • 網絡體積小,運算速度快

目前深度學習模型壓縮方法的研究主要可以分爲以下幾個方向:
(1)更精細模型的設計。目前很多網絡基於模塊化設計思想,在深度和寬度兩個維度上都很大,導致參數冗餘。因此有很多關於模型設計的研究,如SqueezeNet、MobileNet等,使用更加細緻、高效的模型設計,能夠很大程度的減少模型尺寸,並且也具有不錯的性能。
(2)模型裁剪。結構複雜的網絡具有非常好的性能,其參數也存在冗餘,因此對於已訓練好的模型網絡,可以尋找一種有效的評判手段,將不重要的connection或者filter進行裁剪來減少模型的冗餘。
(3)核的稀疏化。在訓練過程中,對權重的更新進行誘導,使其更加稀疏,對於稀疏矩陣,可以使用更加緊緻的存儲方式,如CSC,但是使用稀疏矩陣操作在硬件平臺上運算效率不高,容易受到帶寬的影響,因此加速並不明顯。
(4)量化
(5)Low-rank分解
(6)遷移學習

17.3 目前有哪些深度學習模型優化加速方法?

https://blog.csdn.net/nature553863/article/details/81083955
模型優化加速能夠提升網絡的計算效率,具體包括:
(1)Op-level的快速算法:FFT Conv2d (7x7, 9x9), Winograd Conv2d (3x3, 5x5) 等;
(2)Layer-level的快速算法:Sparse-block net [1] 等;
(3)優化工具與庫:TensorRT (Nvidia), Tensor Comprehension (Facebook) 和 Distiller (Intel) 等;

原文:https://blog.csdn.net/nature553863/article/details/81083955

17.4 影響神經網絡速度的4個因素(再稍微詳細一點)

  1. FLOPs(FLOPs就是網絡執行了多少multiply-adds操作);
  2. MAC(內存訪問成本);
  3. 並行度(如果網絡並行度高,速度明顯提升);
  4. 計算平臺(GPU,ARM)

17.5 改變網絡結構設計爲什麼會實現模型壓縮、加速?

1. Group convolution

Group convolution最早出現在AlexNet中,是爲了解決單卡顯存不夠,將網絡部署到多卡上進行訓練。Group convolution可以減少單個卷積1/g的參數量。
假設輸入特徵的的維度爲H * W * c1;卷積核的維度爲h1 * w1 * c1,共c2個;輸出特徵的維度爲 H1 * W1 * c2。
傳統卷積計算方式如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XJMa2hIp-1575790882677)(./img/ch17/1.png)]
傳統卷積運算量爲:

A = H * W * h1 * w1 * c1 * c2

Group convolution是將輸入特徵的維度c1分成g份,每個group對應的channel數爲c1/g,特徵維度H * W * c1/g;,每個group對應的卷積核的維度也相應發生改變爲h1 * w1 * c1/9,共c2/g個;每個group相互獨立運算,最後將結果疊加在一起。
Group convolution計算方式如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yFvoT3Yr-1575790882734)(./img/ch17/2.png)]
Group convolution運算量爲:

B = H * W * h1 * w1 * c1/g * c2/g * g

Group卷積相對於傳統卷積的運算量:

\dfrac{B}{A} = \dfrac{ H * W * h1 * w1 * c1/g * c2/g * g}{H * W * h1 * w1 * c1 * c2} = \dfrac{1}{g}

由此可知:group卷積相對於傳統卷積減少了1/g的參數量。

2. Depthwise separable convolution

Depthwise separable convolution是由depthwise conv和pointwise conv構成。
depthwise conv(DW)有效減少參數數量並提升運算速度。但是由於每個feature map只被一個卷積核卷積,因此經過DW輸出的feature map不能只包含輸入特徵圖的全部信息,而且特徵之間的信息不能進行交流,導致“信息流通不暢”。
pointwise conv(PW)實現通道特徵信息交流,解決DW卷積導致“信息流通不暢”的問題。
假設輸入特徵的的維度爲H * W * c1;卷積核的維度爲h1 * w1 * c1,共c2個;輸出特徵的維度爲 H1 * W1 * c2。
傳統卷積計算方式如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-L2tARJFG-1575790882736)(./img/ch17/3.png)]
傳統卷積運算量爲:

A = H * W * h1 * w1 * c1 * c2

DW卷積的計算方式如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-q22xTrCn-1575790882737)(./img/ch17/4.png)]
DW卷積運算量爲:

B_DW = H * W * h1 * w1 * 1 * c1

PW卷積的計算方式如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-j8HRoOck-1575790882738)(./img/ch17/5.png)]

B_PW = H_m * W_m * 1 * 1 * c1 * c2

Depthwise separable convolution運算量爲:

B = B_DW + B_PW  

Depthwise separable convolution相對於傳統卷積的運算量:

\dfrac{B}{A} = \dfrac{ H * W * h1 * w1 * 1 * c1 + H_m * W_m * 1 * 1 * c1 * c2}{H * W * h1 * w1 * c1 * c2}  

= \dfrac{1}{c2} + \dfrac{1}{h1 * w1}

由此可知,隨着卷積通道數的增加,Depthwise separable convolution的運算量相對於傳統卷積更少。

3. 輸入輸出的channel相同時,MAC最小

卷積層的輸入和輸出特徵通道數相等時MAC最小,此時模型速度最快。
假設feature map的大小爲h*w,輸入通道c1,輸出通道c2。
已知:

FLOPs = B = h * w * c1 * c2

=> c1 * c2 = \dfrac{B}{h * w}

MAC = h * w * (c1 + c2) + c1 * c2

c1 + c2 \geq 2 * \sqrt{c1 * c2}

=> MAC \geq 2 * h * w \sqrt{\dfrac{B}{h * w}} + \dfrac{B}{h * w} 

根據均值不等式得到(c1-c2)^2>=0,等式成立的條件是c1=c2,也就是輸入特徵通道數和輸出特徵通道數相等時,在給定FLOPs前提下,MAC達到取值的下界。

4. 減少組卷積的數量

過多的group操作會增大MAC,從而使模型速度變慢
由以上公式可知,group卷積想比與傳統的卷積可以降低計算量,提高模型的效率;如果在相同的FLOPs時,group卷積爲了滿足FLOPs會是使用更多channels,可以提高模型的精度。但是隨着channel數量的增加,也會增加MAC。
FLOPs:

B = \dfrac{h * w * c1 * c2}{g}

MAC:

MAC = h * w * (c1 + c2) + \dfrac{c1 * c2}{g}

由MAC,FLOPs可知:

MAC = h * w * c1 + \dfrac{B*g}{c1} + \dfrac{B}{h * w}

當FLOPs固定(B不變)時,g越大,MAC越大。

5. 減少網絡碎片化程度(分支數量)

模型中分支數量越少,模型速度越快
此結論主要是由實驗結果所得。
以下爲網絡分支數和各分支包含的卷積數目對神經網絡速度的影響。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZAzLwVlR-1575790882739)(./img/ch17/6.png)]
實驗中使用的基本網絡結構,分別將它們重複10次,然後進行實驗。實驗結果如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-w6VG3tDa-1575790882741)(./img/ch17/7.png)]
由實驗結果可知,隨着網絡分支數量的增加,神經網絡的速度在降低。網絡碎片化程度對GPU的影響效果明顯,對CPU不明顯,但是網絡速度同樣在降低。

6. 減少元素級操作

元素級操作所帶來的時間消耗也不能忽視
ReLU ,Tensor 相加,Bias相加的操作,分離卷積(depthwise convolution)都定義爲元素級操作。
FLOPs大多數是對於卷積計算而言的,因爲元素級操作的FLOPs相對要低很多。但是過的元素級操作也會帶來時間成本。ShuffleNet作者對ShuffleNet v1和MobileNet v2的幾種層操作的時間消耗做了分析,發現元素級操作對於網絡速度的影響也很大。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LfWFh7JF-1575790882742)(./img/ch17/8.png)]

17.6 常用的輕量級網絡有哪些?(再琢磨下語言和排版)

  • SqueezeNet
  • MobileNet
  • ShuffleNet
  • Xception

1. SequeezeNet

SqueenzeNet出自F. N. Iandola, S.Han等人發表的論文《SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and < 0.5MB model size》,作者在保證精度不損失的同時,將原始AlexNet壓縮至原來的510倍。

1.1 設計思想

在網絡結構設計方面主要採取以下三種方式:

  • 用1*1卷積核替換3*3卷積
    • 理論上一個1*1卷積核的參數是一個3*3卷積核的1/9,可以將模型尺寸壓縮9倍。
  • 減小3*3卷積的輸入通道數
    • 根據上述公式,減少輸入通道數不僅可以減少卷積的運算量,而且輸入通道數與輸出通道數相同時還可以減少MAC。
  • 延遲降採樣
    • 分辨率越大的輸入能夠提供更多特徵的信息,有利於網絡的訓練判斷,延遲降採樣可以提高網絡精度。

1.2 網絡架構

SqueezeNet提出一種多分支結構——fire model,其中是由Squeeze層和expand層構成。Squeeze層是由s1個1*1卷積組成,主要是通過1*1的卷積降低expand層的輸入維度;expand層利用e1個1*1和e3個3*3卷積構成多分支結構提取輸入特徵,以此提高網絡的精度(其中e1=e3=4*s1)。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eC1CmUoY-1575790882743)(./img/ch17/9.png)]
SqueezeNet整體網絡結構如下圖所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-97TapKi7-1575790882744)(./img/ch17/10.png)]

1.3實驗結果

不同壓縮方法在ImageNet上的對比實驗結果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WtmbfcSW-1575790882746)(./img/ch17/11.png)]
由實驗結果可知,SqueezeNet不僅保證了精度,而且將原始AlexNet從240M壓縮至4.8M,壓縮50倍,說明此輕量級網絡設計是可行。

2. MobileNet

MobileNet 是Google團隊於CVPR-2017的論文《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》中針對手機等嵌入式設備提出的一種輕量級的深層神經網絡,該網絡結構在VGG的基礎上使用DW+PW的組合,在保證不損失太大精度的同時,降低模型參數量。

2.1 設計思想

  • 採用深度可分離卷積代替傳統卷積
    • 採用DW卷積在減少參數數量的同時提升運算速度。但是由於每個feature map只被一個卷積核卷積,因此經過DW輸出的feature map不能只包含輸入特徵圖的全部信息,而且特徵之間的信息不能進行交流,導致“信息流通不暢”。
    • 採用PW卷積實現通道特徵信息交流,解決DW卷積導致“信息流通不暢”的問題。
  • 使用stride=2的卷積替換pooling
    • 直接在卷積時利用stride=2完成了下采樣,從而節省了需要再去用pooling再去進行一次下采樣的時間,可以提升運算速度。同時,因爲pooling之前需要一個stride=1的 conv,而與stride=2 conv的計算量想比要高近4倍(個人理解)。

2.2 網絡架構

  • DW conv和PW conv
    MobileNet的網絡架構主要是由DW conv和PW conv組成,相比於傳統卷積可以降低$\dfrac{1}{N} + \dfrac{1}{Dk}$倍的計算量。
    標準卷積與DW conv和PW conv如圖所示:
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9iM3iy1V-1575790882747)(./img/ch17/12.png)]
    深度可分離卷積與傳統卷積運算量對比:
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GDNxZTjM-1575790882748)(./img/ch17/13.png)]
    網絡結構:
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rW8hMsDm-1575790882749)(./img/ch17/14.png)]

  • MobileNets的架構
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LzxTEFn3-1575790882750)(./img/ch17/15.png)]

2.3 實驗結果

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oBFclV6g-1575790882751)(./img/ch17/16.png)]
由上表可知,使用相同的結構,深度可分離卷積雖然準確率降低1%,但是參數量減少了6/7。

3. MobileNet-v2

MobileNet-V2是2018年1月公開在arXiv上論文《Inverted Residuals and Linear Bottlenecks: Mobile Networks for Classification, Detection and Segmentation》,是對MobileNet-V1的改進,同樣是一個輕量化卷積神經網絡。

3.1 設計思想

  • 採用Inverted residuals
    • 爲了保證網絡可以提取更多的特徵,在residual block中第一個1*1 Conv和3*3 DW Conv之前進行通道擴充
  • Linear bottlenecks
    • 爲了避免Relu對特徵的破壞,在residual block的Eltwise sum之前的那個 1*1 Conv 不再採用Relu
  • stride=2的conv不使用shot-cot,stride=1的conv使用shot-cut

3.2 網絡架構

  • Inverted residuals
    ResNet中Residuals block先經過1*1的Conv layer,把feature map的通道數降下來,再經過3*3 Conv layer,最後經過一個1*1 的Conv layer,將feature map 通道數再“擴張”回去。即採用先壓縮,後擴張的方式。而 inverted residuals採用先擴張,後壓縮的方式。
    MobileNet採用DW conv提取特徵,由於DW conv本身提取的特徵數就少,再經過傳統residuals block進行“壓縮”,此時提取的特徵數會更少,因此inverted residuals對其進行“擴張”,保證網絡可以提取更多的特徵。
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sZYxrcIc-1575790882752)(./img/ch17/17.png)]
  • Linear bottlenecks
    ReLu激活函數會破壞特徵。ReLu對於負的輸入,輸出全爲0,而本來DW conv特徵通道已經被“壓縮”,再經過ReLu的話,又會損失一部分特徵。採用Linear,目的是防止Relu破壞特徵。
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Wp892Ilx-1575790882753)(./img/ch17/18.png)]
  • shortcut
    stride=2的conv不使用shot-cot,stride=1的conv使用shot-cut
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ELG700Hb-1575790882754)(./img/ch17/19.png)]
  • 網絡架構
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-B52qttrc-1575790882755)(./img/ch17/20.png)]

4. Xception

Xception是Google提出的,arXiv 的V1 於2016年10月公開《Xception: Deep Learning with Depthwise Separable Convolutions 》,Xception是對Inception v3的另一種改進,主要是採用depthwise separable convolution來替換原來Inception v3中的卷積操作。

4.1設計思想

  • 採用depthwise separable convolution來替換原來Inception v3中的卷積操作
    與原版的Depth-wise convolution有兩個不同之處:
    • 第一個:原版Depth-wise convolution,先逐通道卷積,再1*1卷積; 而Xception是反過來,先1*1卷積,再逐通道卷積;
    • 第二個:原版Depth-wise convolution的兩個卷積之間是不帶激活函數的,而Xception在經過1*1卷積之後會帶上一個Relu的非線性激活函數;

4.2網絡架構

feature map在空間和通道上具有一定的相關性,通過Inception模塊和非線性激活函數實現通道之間的解耦。增多3*3的卷積的分支的數量,使它與1*1的卷積的輸出通道數相等,此時每個3*3的卷積只作用與一個通道的特徵圖上,作者稱之爲“極致的Inception(Extream Inception)”模塊,這就是Xception的基本模塊。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-f3oNNI32-1575790882757)(./img/ch17/21.png)]

5. ShuffleNet-v1

ShuffleNet 是Face++團隊提出的,晚於MobileNet兩個月在arXiv上公開《ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices 》用於移動端前向部署的網絡架構。ShuffleNet基於MobileNet的group思想,將卷積操作限制到特定的輸入通道。而與之不同的是,ShuffleNet將輸入的group進行打散,從而保證每個卷積核的感受野能夠分散到不同group的輸入中,增加了模型的學習能力。

5.1 設計思想

  • 採用group conv減少大量參數
    • roup conv與DW conv存在相同的“信息流通不暢”問題
  • 採用channel shuffle解決上述問題
    • MobileNet中採用PW conv解決上述問題,SheffleNet中採用channel shuffle
  • 採用concat替換add操作
    • avg pooling和DW conv(s=2)會減小feature map的分辨率,採用concat增加通道數從而彌補分辨率減小而帶來信息的損失

5.2 網絡架構

MobileNet中1*1卷積的操作佔據了約95%的計算量,所以作者將1*1也更改爲group卷積,使得相比MobileNet的計算量大大減少。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cVh8EeWr-1575790882758)(./img/ch17/22.png)]
group卷積與DW存在同樣使“通道信息交流不暢”的問題,MobileNet中採用PW conv解決上述問題,SheffleNet中採用channel shuffle。
ShuffleNet的shuffle操作如圖所示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MuA8VKJj-1575790882759)(./img/ch17/24.png)]
avg pooling和DW conv(s=2)會減小feature map的分辨率,採用concat增加通道數從而彌補分辨率減小而帶來信息的損失;實驗表明:多多使用通道(提升通道的使用率),有助於提高小模型的準確率。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Q9WvWW4m-1575790882759)(./img/ch17/23.png)]
網絡結構:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3cbaZ1l0-1575790882760)(./img/ch17/25.png)]

6. ShuffleNet-v2

huffleNet-v2 是Face++團隊提出的《ShuffleNet V2: Practical Guidelines for Ecient CNN Architecture Design》,旨在設計一個輕量級但是保證精度、速度的深度網絡。

6.1 設計思想

  • 文中提出影響神經網絡速度的4個因素:
    • a. FLOPs(FLOPs就是網絡執行了多少multiply-adds操作)
    • b. MAC(內存訪問成本)
    • c. 並行度(如果網絡並行度高,速度明顯提升)
    • d. 計算平臺(GPU,ARM)
  • ShuffleNet-v2 提出了4點網絡結構設計策略:
    • G1.輸入輸出的channel相同時,MAC最小
    • G2.過度的組卷積會增加MAC
    • G3.網絡碎片化會降低並行度
    • G4.元素級運算不可忽視

6.2 網絡結構

depthwise convolution 和 瓶頸結構增加了 MAC,用了太多的 group,跨層連接中的 element-wise Add 操作也是可以優化的點。所以在 shuffleNet V2 中增加了幾種新特性。
所謂的 channel split 其實就是將通道數一分爲2,化成兩分支來代替原先的分組卷積結構(G2),並且每個分支中的卷積層都是保持輸入輸出通道數相同(G1),其中一個分支不採取任何操作減少基本單元數(G3),最後使用了 concat 代替原來的 elementy-wise add,並且後面不加 ReLU 直接(G4),再加入channle shuffle 來增加通道之間的信息交流。 對於下采樣層,在這一層中對通道數進行翻倍。 在網絡結構的最後,即平均值池化層前加入一層 1x1 的卷積層來進一步的混合特徵。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Vli3KEUg-1575790882761)(./img/ch17/26.png)]
網絡結構
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GOJYttMH-1575790882762)(./img/ch17/27.png)]

6.4 ShuffleNet-v2具有高精度的原因

  • 由於高效,可以增加更多的channel,增加網絡容量
  • 採用split使得一部分特徵直接與下面的block相連,特徵複用(DenseNet)

17.7 現有移動端開源框架及其特點

17.7.1 NCNN

1、開源時間:2017年7月

2、開源用戶:騰訊優圖

3、GitHub地址:https://github.com/Tencent/ncnn

4、特點:

  • 1)NCNN考慮了手機端的硬件和系統差異以及調用方式,架構設計以手機端運行爲主要原則。
  • 2)無第三方依賴,跨平臺,手機端 CPU 的速度快於目前所有已知的開源框架(以開源時間爲參照對象)。
  • 3)基於 ncnn,開發者能夠將深度學習算法輕鬆移植到手機端高效執行,開發出人工智能 APP。

5、功能:

  • 1、NCNN支持卷積神經網絡、多分支多輸入的複雜網絡結構,如vgg、googlenet、resnet、squeezenet 等。
  • 2、NCNN無需依賴任何第三方庫。
  • 3、NCNN全部使用C/C++實現,以及跨平臺的cmake編譯系統,可輕鬆移植到其他系統和設備上。
  • 4、彙編級優化,計算速度極快。使用ARM NEON指令集實現卷積層,全連接層,池化層等大部分 CNN 關鍵層。
  • 5、精細的數據結構設計,沒有采用需消耗大量內存的通常框架——im2col + 矩陣乘法,使得內存佔用極低。
  • 6、支持多核並行計算,優化CPU調度。
  • 7、整體庫體積小於500K,可精簡到小於300K。
  • 8、可擴展的模型設計,支持8bit 量化和半精度浮點存儲。
  • 9、支持直接內存引用加載網絡模型。
  • 10、可註冊自定義層實現並擴展。

6、NCNN在Android端部署示例

  • 1)選擇合適的Android Studio版本並安裝。
  • 2)根據需求選擇NDK版本並安裝。
  • 3)在Android Studio上配置NDK的環境變量。
  • 4)根據自己需要編譯NCNN sdk
mkdir build-android cd build-android cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI="armeabi-v7a" -DANDROID_ARM_NEON=ON \ -DANDROID_PLATFORM=android-14 .. make make install

​ 安裝完成之後,install下有include和lib兩個文件夾。

​ 備註:

ANDROID_ABI 是架構名字,"armeabi-v7a" 支持絕大部分手機硬件 
ANDROID_ARM_NEON 是否使用 NEON 指令集,設爲 ON 支持絕大部分手機硬件 
ANDROID_PLATFORM 指定最低系統版本,"android-14" 就是 android-4.0
  • 5)進行NDK開發。
1)assets文件夾下放置你的bin和param文件。
2)jni文件夾下放置你的cpp和mk文件。
3)修改你的app gradle文件。
4)配置Android.mk和Application.mk文件。
5)進行java接口的編寫。
6)讀取拷貝bin和param文件(有些則是pb文件,根據實際情況)。
7)進行模型的初始化和執行預測等操作。
8)build。
9)cd到src/main/jni目錄下,執行ndk-build,生成.so文件。
10)接着就可寫自己的操作處理需求。

17.7.2 QNNPACK

全稱:Quantized Neural Network PACKage(量化神經網絡包)

1、開源時間:2018年10月

2、開源用戶:Facebook

3、GitHub地址:https://github.com/pytorch/QNNPACK

4、特點:

​ 1)低密度卷積優化函數庫;

2)可在手機上實時運行Mask R-CNN 和 DensePose;

​ 3) 能在性能受限的移動設備中用 100ms 以內的時間實施圖像分類;

5、QNNPACK 如何提高效率?

1)QNNPACK 使用與安卓神經網絡 API 兼容的線性量化方案

QNNPACK 的輸入矩陣來自低精度、移動專用的計算機視覺模型。其它庫在計算A和B矩陣相乘時,重新打包 A 和 B 矩陣以更好地利用緩存層次結構,希望在大量計算中分攤打包開銷,QNNPACK 刪除所有計算非必需的內存轉換,針對 A和B矩陣相乘適用於一級緩存的情況進行了優化。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qNGDTddd-1575790882763)(./img/ch17/QNNPACK1.jpeg)]

​ 在量化矩陣-矩陣乘法中,8 位整數的乘積通常會被累加至 32 位的中間結果中,隨後重新量化以產生 8 位的輸出。常規的實現會對大矩陣尺寸進行優化——有時 K 太大無法將 A 和 B 的面板轉入緩存中。爲了有效利用緩存層次結構,傳統的 GEMM 實現將 A 和 B 的面板沿 K 維分割成固定大小的子面板,從而每個面板都適應 L1 緩存,隨後爲每個子面板調用微內核。這一緩存優化需要 PDOT 爲內核輸出 32 位中間結果,最終將它們相加並重新量化爲 8 位整數。

​ 由於 ONNPACK 對於面板 A 和 B 總是適應 L1 緩存的移動神經網絡進行了優化,因此它在調用微內核時處理整個 A 和 B 的面板。而由於無需在微內核之外積累 32 位的中間結果,QNNPACK 會將 32 位的中間結果整合進微內核中並寫出 8 位值,這節省了內存帶寬和緩存佔用。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OYk3W3gw-1575790882764)(./img/ch17/QNNPACK2.jpeg)]

使整個 A、B 面板適配緩存幫助實現了 QNNPACK 中的另一個優化:取消了矩陣 A 的重新打包。矩陣 B 包含靜態權重,可以一次性轉換成任何內存佈局,但矩陣 A 包含卷積輸入,每次推理運行都會改變。因此,重新打包矩陣 A 在每次運行時都會產生開銷。儘管存在開銷,傳統的 GEMM 實現還是出於以下兩個原因對矩陣 A 進行重新打包:緩存關聯性及微內核效率受限。如果不重新打包,微內核將不得不讀取被潛在的大跨距隔開的幾行 A。如果這個跨距恰好是 2 的許多次冪的倍數,面板中不同行 A 的元素可能會落入同一緩存集中。如果衝突的行數超過了緩存關聯性,它們就會相互驅逐,性能也會大幅下降。幸運的是,當面板適配一級緩存時,這種情況不會發生,就像 QNNPACK 優化的模型一樣。

打包對微內核效率的影響與當前所有移動處理器支持的 SIMD 向量指令的使用密切相關。這些指令加載、存儲或者計算小型的固定大小元素向量,而不是單個標量(scalar)。在矩陣相乘中,充分利用向量指令達到高性能很重要。在傳統的 GEMM 實現中,微內核把 MR 元素重新打包到向量暫存器裏的 MR 線路中。在 QNNPACK 實現中,MR 元素在存儲中不是連續的,微內核需要把它們加載到不同的向量暫存器中。越來越大的暫存器壓力迫使 QNNPACK 使用較小的 MRxNR 拼貼,但實際上這種差異很小,而且可以通過消除打包開銷來補償。例如,在 32 位 ARM 架構上,QNNPACK 使用 4×8 微內核,其中 57% 的向量指令是乘-加;另一方面,gemmlowp 庫使用效率稍高的 4×12 微內核,其中 60% 的向量指令是乘-加。

微內核加載 A 的多個行,乘以 B 的滿列,結果相加,然後完成再量化並記下量化和。A 和 B 的元素被量化爲 8 位整數,但乘積結果相加到 32 位。大部分 ARM 和 ARM64 處理器沒有直接完成這一運算的指令,所以它必須分解爲多個支持運算。QNNPACK 提供微內核的兩個版本,其不同之處在於用於乘以 8 位值並將它們累加到 32 位的指令序列。

2)從矩陣相乘到卷積

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-o9PurJHb-1575790882765)(./img/ch17/QNNPACK3.jpeg)]

簡單的 1×1 卷積可直接映射到矩陣相乘,但對於具備較大卷積核、padding 或子採樣(步幅)的卷積而言則並非如此。但是,這些較複雜的卷積能夠通過記憶變換 im2col 映射到矩陣相乘。對於每個輸出像素,im2col 複製輸入圖像的圖像塊並將其計算爲 2D 矩陣。由於每個輸出像素都受 KHxKWxC 輸入像素值的影響(KH 和 KW 分別指卷積核的高度和寬度,C 指輸入圖像中的通道數),因此該矩陣的大小是輸入圖像的 KHxKW 倍,im2col 給內存佔用和性能都帶來了一定的開銷。和 Caffe 一樣,大部分深度學習框架轉而使用基於 im2col 的實現,利用現有的高度優化矩陣相乘庫來執行卷積操作。

Facebook 研究者在 QNNPACK 中實現了一種更高效的算法。他們沒有變換卷積輸入使其適應矩陣相乘的實現,而是調整 PDOT 微內核的實現,在運行中執行 im2col 變換。這樣就無需將輸入張量的實際輸入複製到 im2col 緩存,而是使用輸入像素行的指針設置 indirection buffer,輸入像素與每個輸出像素的計算有關。研究者還修改了矩陣相乘微內核,以便從 indirection buffer 加載虛構矩陣(imaginary matrix)A 的行指針,indirection buffer 通常比 im2col buffer 小得多。此外,如果兩次推斷運行的輸入張量存儲位置不變,則 indirection buffer 還可使用輸入張量行的指針進行初始化,然後在多次推斷運行中重新使用。研究者觀察到具備 indirection buffer 的微內核不僅消除了 im2col 變換的開銷,其性能也比矩陣相乘微內核略好(可能由於輸入行在計算不同輸出像素時被重用)。

3)深度卷積

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-t0VMQhLD-1575790882766)(./img/ch17/QNNPACK4.jpeg)]

分組卷積(grouped convolution)將輸入和輸出通道分割成多組,然後對每個組進行分別處理。在有限條件下,當組數等於通道數時,該卷積就是深度卷積,常用於當前的神經網絡架構中。深度卷積對每個通道分別執行空間濾波,展示了與正常卷積非常不同的計算模式。因此,通常要向深度卷積提供單獨實現,QNNPACK 包括一個高度優化版本 3×3 深度卷積。

深度卷積的傳統實現是每次都在卷積核元素上迭代,然後將一個卷積核行和一個輸入行的結果累加到輸出行。對於一個 3×3 的深度卷積,此類實現將把每個輸出行更新 9 次。在 QNNPACK 中,研究者計算所有 3×3 卷積核行和 3×3 輸入行的結果,一次性累加到輸出行,然後再處理下個輸出行。

QNNPACK 實現高性能的關鍵因素在於完美利用通用暫存器(GPR)來展開卷積核元素上的循環,同時避免在 hot loop 中重新加載地址寄存器。32-bit ARM 架構將實現限制在 14 個 GPR。在 3×3 深度卷積中,需要讀取 9 個輸入行和 9 個卷積核行。這意味着如果想完全展開循環必須存儲 18 個地址。然而,實踐中推斷時卷積核不會發生變化。因此 Facebook 研究者使用之前在 CxKHxKW 中的濾波器,將它們封裝進 [C/8]xKWxKHx8,這樣就可以僅使用具備地址增量(address increment)的一個 GPR 訪問所有濾波器。(研究者使用數字 8 的原因在於,在一個命令中加載 8 個元素然後減去零,在 128-bit NEON 暫存器中生成 8 個 16-bit 值。)然後使用 9 個輸入行指針,指針將濾波器重新裝進 10 個 GPR,完全展開濾波器元素上的循環。64-bit ARM 架構相比 32-bit 架構,GPR 的數量翻了一倍。QNNPACK 利用額外的 ARM64 GPR,一次性存儲 3×5 輸入行的指針,並計算 3 個輸出行。

7、性能優勢:

​ 測試結果顯示出 QNNPACK 在端到端基準上的性能優勢。在量化當前最優 MobileNetV2 架構上,基於QNNPACK 的 Caffe2 算子的速度大約是 TensorFlow Lite 速度的 2 倍,在多種手機上都是如此。除了 QNNPACK 之外,Facebook 還開源了 Caffe2 quantized MobileNet v2 模型,其 top-1 準確率比相應的 TensorFlow 模型高出 1.3%。

MobileNetV1

MobileNetV1 架構在使用深度卷積(depthwise convolution)使模型更適合移動設備方面具備開創性。MobileNetV1 包括幾乎整個 1×1 卷積和 3×3 卷積。Facebook 研究者將量化 MobileNetV1 模型從 TensorFlow Lite 轉換而來,並在 TensorFlow Lite 和 QNNPACK 的 32-bit ARM 設備上對 MobileNetV1 進行基準測試。二者運行時均使用 4 線程,研究者觀察到 QNNPACK 的運行速度幾何平均值是 TensorFlow Lite 的 1.8 倍。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-riylmU8H-1575790882767)(./img/ch17/mv1.jpg)]

MobileNetV2

作爲移動視覺任務的當前最優架構之一,MobileNetV2 引入了瓶頸構造塊和瓶頸之間的捷徑連接。研究者在 MobileNetV2 分類模型的量化版上對比基於 QNNPACK 的 Caffe2 算子和 TensorFlow Lite 實現。使用的量化 Caffe2 MobileNetV2 模型已開源,量化 TensorFlow Lite 模型來自官方庫:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/lite/g3doc/models.md。下表展示了二者在常用測試集上的 top1 準確率:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GcJKnpC9-1575790882767)(./img/ch17/mv2.jpg)]

​ Facebook 研究者利用這些模型建立了 Facebook AI 性能評估平臺(https://github.com/facebook/FAI-PEP)的基準,該基準基於 32-bit ARM 環境的大量手機設備。對於 TensorFlow Lite 線程設置,研究者嘗試了一到四個線程,並報告了最快速的結果。結果顯示 TensorFlow Lite 使用四線程的性能最優,因此後續研究中使用四線程來對比 TensorFlow Lite 和 QNNPACK。下表展示了結果,以及在典型智能手機和高端機上,基於 QNNPACK 的算子速度比 TensorFlow Lite 快得多。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ERnkEuYB-1575790882768)(./img/ch17/mv3.jpg)]

Facebook開源高性能內核庫QNNPACK
https://baijiahao.baidu.com/s?id=1615725346726413945&wfr=spider&for=pc
http://www.sohu.com/a/272158070_610300

支持移動端深度學習的幾種開源框架
https://blog.csdn.net/zchang81/article/details/74280019

17.7.3 Prestissimo

1、開源時間:2017年11月

2、開源用戶:九言科技

3、GitHub地址:https://github.com/in66-dev/In-Prestissimo

4、功能特點:

基礎功能

  • 支持卷積神經網絡,支持多輸入和多分支結構
  • 精煉簡潔的API設計,使用方便
  • 提供調試接口,支持打印各個層的數據以及耗時
  • 不依賴任何第三方計算框架,整體庫體積 500K 左右(32位 約400k,64位 約600k)
  • 純 C++ 實現,跨平臺,支持 android 和 ios
  • 模型爲純二進制文件,不暴露開發者設計的網絡結構

極快的速度

  • 大到框架設計,小到彙編書寫上全方位的優化,iphone7 上跑 SqueezeNet 僅需 26ms(單線程)
  • 支持浮點(float)和整型(int)兩種運算模式,float模式精度與caffe相同,int模式運算速度快,大部分網絡用int的精度便已經足夠
  • 以巧妙的內存佈局提升cpu的cache命中率,在中低端機型上性能依然強勁
  • 針對 float-arm32, float-arm64, int-arm32, int-arm64 四個分支均做了細緻的優化,保證arm32位和arm64位版本都有非常好的性能

SqueezeNet-v1.1 測試結果

Note: 手機測試性能存在一定的抖動,連續多次運算取平均時間

Note: 像華爲mate8, mate9,Google nexus 6 雖然是64位的CPU,但測試用的是 32位的庫,因此cpu架構依然寫 arm-v7a

CPU架構 機型 CPU ncnn(4線程) mdl Prestissimo_float(單線程) Prestissimo_int(單線程)
arm-v7a 小米2 高通APQ8064 1.5GHz 185 ms 370 ms 184 ms 115 ms
arm-v7a 小米2s 四核 驍龍APQ8064 Pro 1.7GHz 166 ms - 136 ms 96 ms
arm-v7a 紅米Note 4x 驍龍625 四核2.0GHz 124 ms 306 ms 202 ms 110 ms
arm-v7a Google Nexus 6 驍龍805 四核 2.7GHz 84 ms 245 ms 103 ms 63 ms
arm-v7a Vivo x6d 聯發科 MT6752 1.7GHz 245 ms 502 ms 370 ms 186 ms
arm-v7a 華爲 Mate 8 海思麒麟950 4大4小 2.3GHz 1.8GHz 75 ms 180 ms 95 ms 57 ms
arm-v7a 華爲 Mate 9 海思麒麟960 4大4小 2.4GHz 1.8GHz 61 ms 170 ms 94 ms 48 ms
arm-v8 iphone7 Apple A10 Fusion 2.34GHz - - 27 ms 26 ms

未開放特性

  • 多核並行加速(多核機器可以再提升30%-100% 的速度)
  • depthwise卷積運算(支持mobilenet)
  • 模型壓縮功能,壓縮後的模型體積可縮小到20%以下
  • GPU 運算模式(Android 基於opengl es 3.1,ios 基於metal)

同類框架對比

框架 caffe tensorflow mdl-android mdl-ios ncnn CoreML Prestissimo
計算硬件 cpu cpu cpu gpu cpu gpu cpu (gpu版本未開放)
計算速度 很快 很快 極快 極快
庫大小 較大 中等
兼容性 限ios8以上 很好 僅支持 ios11 很好
模型支持度 很好 - 差(僅限指定模型) 較好 - 中等(當前版本不支持mobilenet)

使用方法-模型轉換

絕影支持的是私有的模型文件格式,需要把 caffe 訓練出來的模型轉換爲 .prestissimo 格式,模型轉換工具爲 caffe2Prestissimo.out。caffe2Prestissimo.out 依賴 protobuf 3.30。將 XXX.prototxt 和 YYY.caffemodel 轉化爲 Prestissimo 模型 ZZZ.prestissimo:(得到)./caffe2Prestissimo.out XXX.prototxt YYY.caffemodel ZZZ.prestissimo

17.7.4 MDL(mobile-deep-learning)

1、開源時間:2017年9月(已暫停更新)

2、開源用戶:百度

3、GitHub地址:https://github.com/allonli/mobile-deep-learning

4、功能特點:

  • 一鍵部署,腳本參數就可以切換ios或者android
  • 支持iOS gpu運行MobileNet、squeezenet模型
  • 已經測試過可以穩定運行MobileNet、GoogLeNet v1、squeezenet、ResNet-50模型
  • 體積極小,無任何第三方依賴。純手工打造。
  • 提供量化函數,對32位float轉8位uint直接支持,模型體積量化後4M上下
  • 與ARM相關算法團隊線上線下多次溝通,針對ARM平臺會持續優化
  • NEON使用涵蓋了卷積、歸一化、池化所有方面的操作
  • 彙編優化,針對寄存器彙編操作具體優化
  • loop unrolling 循環展開,爲提升性能減少不必要的CPU消耗,全部展開判斷操作
  • 將大量繁重的計算任務前置到overhead過程

5、框架結構

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PCqjTS4r-1575790882769)(./img/ch17/MDL1.png)]

MDL 框架主要包括:模型轉換模塊(MDL Converter)、模型加載模塊(Loader)、網絡管理模塊(Net)、矩陣運算模塊(Gemmers)及供 Android 端調用的 JNI 接口層(JNI Interfaces)。

​ 其中,模型轉換模塊主要負責將Caffe 模型轉爲 MDL 模型,同時支持將 32bit 浮點型參數量化爲 8bit 參數,從而極大地壓縮模型體積;模型加載模塊主要完成模型的反量化及加載校驗、網絡註冊等過程,網絡管理模塊主要負責網絡中各層 Layer 的初始化及管理工作;MDL 提供了供 Android 端調用的 JNI 接口層,開發者可以通過調用 JNI 接口輕鬆完成加載及預測過程。

6、MDL 的性能及兼容性

  • 體積 armv7 300k+
  • 速度 iOS GPU mobilenet 可以達到 40ms、squeezenet 可以達到 30ms

​ MDL 從立項到開源,已經迭代了一年多。移動端比較關注的多個指標都表現良好,如體積、功耗、速度。百度內部產品線在應用前也進行過多次對比,和已開源的相關項目對比,MDL 能夠在保證速度和能耗的同時支持多種深度學習模型,如 mobilenet、googlenet v1、squeezenet 等,且具有 iOS GPU 版本,squeezenet 一次運行最快可以達到 3-40ms。

同類框架對比

​ 框架Caffe2TensorFlowncnnMDL(CPU)MDL(GPU)硬件CPUCPUCPUCPUGPU速度慢慢快快極快體積大大小小小兼容Android&iOSAndroid&iOSAndroid&iOSAndroid&iOSiOS

​ 與支持 CNN 的移動端框架對比,MDL 速度快、性能穩定、兼容性好、demo 完備。

兼容性

​ MDL 在 iOS 和 Android 平臺均可以穩定運行,其中 iOS10 及以上平臺有基於 GPU 運算的 API,性能表現非常出色,在 Android 平臺則是純 CPU 運行。高中低端機型運行狀態和手機百度及其他 App 上的覆蓋都有絕對優勢。

​ MDL 同時也支持 Caffe 模型直接轉換爲 MDL 模型。

17.7.5 Paddle-Mobile

1、開源時間:持續更新,已到3.0版本

2、開源用戶:百度

3、GitHub地址:https://github.com/PaddlePaddle/paddle-mobile

4、功能特點:

功能特點

  • 高性能支持ARM CPU

  • 支持Mali GPU

  • 支持Andreno GPU

  • 支持蘋果設備的GPU Metal實現

  • 支持ZU5、ZU9等FPGA開發板

  • 支持樹莓派等arm-linux開發板

17.7.6 MACE( Mobile AI Compute Engine)

1、開源時間:2018年4月(持續更新,v0.9.0 (2018-07-20))

2、開源用戶:小米

3、GitHub地址:https://github.com/XiaoMi/mace

4、簡介:Mobile AI Compute Engine (MACE) 是一個專爲移動端異構計算設備優化的深度學習前向預測框架。
MACE覆蓋了常見的移動端計算設備(CPU,GPU和DSP),並且提供了完整的工具鏈和文檔,用戶藉助MACE能夠很方便地在移動端部署深度學習模型。MACE已經在小米內部廣泛使用並且被充分驗證具有業界領先的性能和穩定性。

5、MACE的基本框架:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dXHlrOYJ-1575790882770)(./img/ch17/mace-arch.png)]

MACE Model

MACE定義了自有的模型格式(類似於Caffe2),通過MACE提供的工具可以將Caffe和TensorFlow的模型 轉爲MACE模型。

MACE Interpreter

MACE Interpreter主要負責解析運行神經網絡圖(DAG)並管理網絡中的Tensors。

Runtime

CPU/GPU/DSP Runtime對應於各個計算設備的算子實現。

6、MACE使用的基本流程

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9UuywLHC-1575790882771)(./img/ch17/mace-work-flow-zh.png)]

1. 配置模型部署文件(.yml)

模型部署文件詳細描述了需要部署的模型以及生成庫的信息,MACE根據該文件最終生成對應的庫文件。

2.編譯MACE庫

編譯MACE的靜態庫或者動態庫。

3.轉換模型

將TensorFlow 或者 Caffe的模型轉爲MACE的模型。

4.1. 部署

根據不同使用目的集成Build階段生成的庫文件,然後調用MACE相應的接口執行模型。

4.2. 命令行運行

MACE提供了命令行工具,可以在命令行運行模型,可以用來測試模型運行時間,內存佔用和正確性。

4.3. Benchmark

MACE提供了命令行benchmark工具,可以細粒度的查看模型中所涉及的所有算子的運行時間。

7、MACE在哪些角度進行了優化?

MACE 專爲移動端異構計算平臺優化的神經網絡計算框架。主要從以下的角度做了專門的優化:

  • 性能

    • 代碼經過NEON指令,OpenCL以及Hexagon HVX專門優化,並且採用
      Winograd算法來進行卷積操作的加速。
      此外,還對啓動速度進行了專門的優化。
  • 功耗

    • 支持芯片的功耗管理,例如ARM的big.LITTLE調度,以及高通Adreno GPU功耗選項。
  • 系統響應

    • 支持自動拆解長時間的OpenCL計算任務,來保證UI渲染任務能夠做到較好的搶佔調度,
      從而保證系統UI的相應和用戶體驗。
  • 內存佔用

    • 通過運用內存依賴分析技術,以及內存複用,減少內存的佔用。另外,保持儘量少的外部
      依賴,保證代碼尺寸精簡。
  • 模型加密與保護

    • 模型保護是重要設計目標之一。支持將模型轉換成C++代碼,以及關鍵常量字符混淆,增加逆向的難度。
  • 硬件支持範圍

    • 支持高通,聯發科,以及松果等系列芯片的CPU,GPU與DSP(目前僅支持Hexagon)計算加速。
    • 同時支持在具有POSIX接口的系統的CPU上運行。

8、性能對比:

MACE 支持 TensorFlow 和 Caffe 模型,提供轉換工具,可以將訓練好的模型轉換成專有的模型數據文件,同時還可以選擇將模型轉換成C++代碼,支持生成動態庫或者靜態庫,提高模型保密性。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fMThhSvG-1575790882772)(./img/ch17/maca_com.jpg)]

17.7.7 FeatherCNN

1、開源時間:持續更新,已到3.0版本

2、開源用戶:騰訊AI

3、GitHub地址:https://github.com/Tencent/FeatherCNN

4、功能特點:

FeatherCNN 是由騰訊 AI 平臺部研發的基於 ARM 架構的高效 CNN 推理庫,該項目支持 Caffe 模型,且具有高性能、易部署、輕量級三大特性。

該項目具體特性如下:

  • 高性能:無論是在移動設備(iOS / Android),嵌入式設備(Linux)還是基於 ARM 的服務器(Linux)上,FeatherCNN 均能發揮最先進的推理計算性能;

  • 易部署:FeatherCNN 的所有內容都包含在一個代碼庫中,以消除第三方依賴關係。因此,它便於在移動平臺上部署。FeatherCNN 自身的模型格式與 Caffe 模型完全兼容。

  • 輕量級:編譯後的 FeatherCNN 庫的體積僅爲數百 KB。

17.7.8 TensorFlow Lite

1、開源時間:2017年11月

2、開源用戶:谷歌

3、GitHub地址:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/lite

4、簡介:

Google 表示 Lite 版本 TensorFlow 是 TensorFlow Mobile 的一個延伸版本。此前,通過TensorFlow Mobile API,TensorFlow已經支持手機上的模型嵌入式部署。TensorFlow Lite應該被視爲TensorFlow Mobile的升級版。

TensorFlow Lite可以與Android 8.1中發佈的神經網絡API完美配合,即便在沒有硬件加速時也能調用CPU處理,確保模型在不同設備上的運行。 而Android端版本演進的控制權是掌握在谷歌手中的,從長期看,TensorFlow Lite會得到Android系統層面上的支持。

5、架構:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cozqf4vg-1575790882773)(./img/ch17/tflite_artc.JPEG)]

其組件包括:

  • TensorFlow 模型(TensorFlow Model):保存在磁盤中的訓練模型。
  • TensorFlow Lite 轉化器(TensorFlow Lite Converter):將模型轉換成 TensorFlow Lite 文件格式的項目。
  • TensorFlow Lite 模型文件(TensorFlow Lite Model File):基於 FlatBuffers,適配最大速度和最小規模的模型。

6、移動端開發步驟:

Android Studio 3.0, SDK Version API26, NDK Version 14

步驟:

  1. 將此項目導入到Android Studio:
    https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/lite/java/demo

  2. 下載移動端的模型(model)和標籤數據(lables):
    https://storage.googleapis.com/download.tensorflow.org/models/tflite/mobilenet_v1_224_android_quant_2017_11_08.zip

  3. 下載完成解壓mobilenet_v1_224_android_quant_2017_11_08.zip文件得到一個xxx.tflite和labes.txt文件,分別是模型和標籤文件,並且把這兩個文件複製到assets文件夾下。

  4. 構建app,run……

17.7.9 TensorFlow Lite和TensorFlow Mobile的區別?

  • TensorFlow Lite是TensorFlow Mobile的進化版。
  • 在大多數情況下,TensorFlow Lite擁有跟小的二進制大小,更少的依賴以及更好的性能。
  • 相比TensorFlow Mobile是對完整TensorFlow的裁減,TensorFlow Lite基本就是重新實現了。從內部實現來說,在TensorFlow內核最基本的OP,Context等數據結構,都是新的。從外在表現來說,模型文件從PB格式改成了FlatBuffers格式,TensorFlow的size有大幅度優化,降至300K,然後提供一個converter將普通TensorFlow模型轉化成TensorFlow Lite需要的格式。因此,無論從哪方面看,TensorFlow Lite都是一個新的實現方案。

17.7.9 PocketFlow

1、開源時間:2018年9月

2、開源用戶:騰訊

3、GitHub地址:https://github.com/Tencent/PocketFlow

4、簡介:

全球首個自動模型壓縮框架

一款面向移動端AI開發者的自動模型壓縮框架,集成了當前主流的模型壓縮與訓練算法,結合自研超參數優化組件實現了全程自動化託管式的模型壓縮與加速。開發者無需瞭解具體算法細節,即可快速地將AI技術部署到移動端產品上,實現了自動託管式模型壓縮與加速,實現用戶數據的本地高效處理。

5、框架介紹

PocketFlow 框架主要由兩部分組件構成,分別是模型壓縮/加速算法組件和超參數優化組件,具體結構如下圖所示。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-g20sdHp1-1575790882774)(./img/ch17/framework_design.png)]

​ 開發者將未壓縮的原始模型作爲 PocketFlow 框架的輸入,同時指定期望的性能指標,例如模型的壓縮和/或加速倍數;在每一輪迭代過程中,超參數優化組件選取一組超參數取值組合,之後模型壓縮/加速算法組件基於該超參數取值組合,對原始模型進行壓縮,得到一個壓縮後的候選模型;基於對候選模型進行性能評估的結果,超參數優化組件調整自身的模型參數,並選取一組新的超參數取值組合,以開始下一輪迭代過程;當迭代終止時,PocketFlow 選取最優的超參數取值組合以及對應的候選模型,作爲最終輸出,返回給開發者用作移動端的模型部署。

6、PocketFlow如何實現模型壓縮與加速?

​ 具體地,PocketFlow 通過下列各個算法組件的有效結合,實現了精度損失更小、自動化程度更高的深度學習模型的壓縮與加速:

  • a) 通道剪枝(channel pruning)組件:在CNN網絡中,通過對特徵圖中的通道維度進行剪枝,可以同時降低模型大小和計算複雜度,並且壓縮後的模型可以直接基於現有的深度學習框架進行部署。在CIFAR-10圖像分類任務中,通過對 ResNet-56 模型進行通道剪枝,可以實現2.5倍加速下分類精度損失0.4%,3.3倍加速下精度損失0.7%。

  • b) 權重稀疏化(weight sparsification)組件:通過對網絡權重引入稀疏性約束,可以大幅度降低網絡權重中的非零元素個數;壓縮後模型的網絡權重可以以稀疏矩陣的形式進行存儲和傳輸,從而實現模型壓縮。對於 MobileNet 圖像分類模型,在刪去50%網絡權重後,在 ImageNet 數據集上的 Top-1 分類精度損失僅爲0.6%。

  • c) 權重量化(weight quantization)組件:通過對網絡權重引入量化約束,可以降低用於表示每個網絡權重所需的比特數;團隊同時提供了對於均勻和非均勻兩大類量化算法的支持,可以充分利用 ARM 和 FPGA 等設備的硬件優化,以提升移動端的計算效率,併爲未來的神經網絡芯片設計提供軟件支持。以用於 ImageNet 圖像分類任務的 ResNet-18 模型爲例,在8比特定點量化下可以實現精度無損的4倍壓縮。

  • d)網絡蒸餾(network distillation)組件:對於上述各種模型壓縮組件,通過將未壓縮的原始模型的輸出作爲額外的監督信息,指導壓縮後模型的訓練,在壓縮/加速倍數不變的前提下均可以獲得0.5%-2.0%不等的精度提升。

  • e) 多GPU訓練(multi-GPU training)組件:深度學習模型訓練過程對計算資源要求較高,單個GPU難以在短時間內完成模型訓練,因此團隊提供了對於多機多卡分佈式訓練的全面支持,以加快使用者的開發流程。無論是基於 ImageNet 數據的Resnet-50圖像分類模型還是基於 WMT14 數據的 Transformer 機器翻譯模型,均可以在一個小時內訓練完畢。[1]

  • f) 超參數優化(hyper-parameter optimization)組件:多數開發者對模型壓縮算法往往不甚瞭解,但超參數取值對最終結果往往有着巨大的影響,因此團隊引入了超參數優化組件,採用了包括強化學習等算法以及 AI Lab 自研的 AutoML 自動超參數優化框架來根據具體性能需求,確定最優超參數取值組合。例如,對於通道剪枝算法,超參數優化組件可以自動地根據原始模型中各層的冗餘程度,對各層採用不同的剪枝比例,在保證滿足模型整體壓縮倍數的前提下,實現壓縮後模型識別精度的最大化。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ROUtS46E-1575790882775)(./img/ch17/packflow1.jpg)]

7、PocketFlow 性能

​ 通過引入超參數優化組件,不僅避免了高門檻、繁瑣的人工調參工作,同時也使得 PocketFlow 在各個壓縮算法上全面超過了人工調參的效果。以圖像分類任務爲例,在 CIFAR-10 和 ImageNet 等數據集上,PocketFlow 對 ResNet 和 MobileNet 等多種 CNN 網絡結構進行有效的模型壓縮與加速。

​ 在 CIFAR-10 數據集上,PocketFlow 以 ResNet-56 作爲基準模型進行通道剪枝,並加入了超參數優化和網絡蒸餾等訓練策略,實現了 2.5 倍加速下分類精度損失 0.4%,3.3 倍加速下精度損失 0.7%,且顯著優於未壓縮的 ResNet-44 模型; 在 ImageNet 數據集上,PocketFlow 可以對原本已經十分精簡的 MobileNet 模型繼續進行權重稀疏化,以更小的模型尺寸取得相似的分類精度;與 Inception-V1、ResNet-18 等模型相比,模型大小僅爲後者的約 20~40%,但分類精度基本一致(甚至更高)。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UcRgnMOt-1575790882780)(./img/ch17/packflow2.jpg)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-64j3yd1v-1575790882781)(./img/ch17/packflow3.jpg)]

相比於費時費力的人工調參,PocketFlow 框架中的 AutoML 自動超參數優化組件僅需 10
餘次迭代就能達到與人工調參類似的性能,在經過 100 次迭代後搜索得到的超參數組合可以降低約 0.6%
的精度損失;通過使用超參數優化組件自動地確定網絡中各層權重的量化比特數,PocketFlow 在對用於 ImageNet 圖像分類任務的
ResNet-18 模型進行壓縮時,取得了一致性的性能提升;當平均量化比特數爲 4 比特時,超參數優化組件的引入可以將分類精度從 63.6%
提升至 68.1%(原始模型的分類精度爲 70.3%)。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-F7w70UcY-1575790882782)(./img/ch17/packflow4.jpg)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HXEz8IjE-1575790882785)(./img/ch17/packflow5.jpg)]

參考文獻

[1] Zhuangwei Zhuang, Mingkui Tan, Bohan Zhuang, Jing Liu, Jiezhang Cao, Qingyao Wu, Junzhou Huang, Jinhui Zhu,「Discrimination-aware Channel Pruning for Deep Neural Networks", In Proc. of the 32nd Annual Conference on Neural Information Processing Systems, NIPS '18, Montreal, Canada, December 2018.

[2] Jiaxiang Wu, Weidong Huang, Junzhou Huang, Tong Zhang,「Error Compensated Quantized SGD and its Applications to Large-scale Distributed Optimization」, In Proc. of the 35th International Conference on Machine Learning, ICML’18, Stockholm, Sweden, July 2018.

17.7.10 其他幾款支持移動端深度學習的開源框架

https://blog.csdn.net/zchang81/article/details/74280019

17.7.11 MDL、NCNN和 TFLite比較

百度-MDL框架、騰訊-NCNN框架和谷歌TFLite框架比較。

MDL NCNN TFLite
代碼質量 很高
跨平臺
支持caffe模型 ×
支持TensorFlow模型 × ×
CPU NEON指令優化
GPU加速 × ×

相同點:

  • 只含推理(inference)功能,使用的模型文件需要通過離線的方式訓練得到。
  • 最終生成的庫尺寸較小,均小於500kB。
  • 爲了提升執行速度,都使用了ARM NEON指令進行加速。
  • 跨平臺,iOS和Android系統都支持。

不同點:

  • MDL和NCNN均是隻支持Caffe框架生成的模型文件,而TfLite則毫無意外的只支持自家大哥TensorFlow框架生成的模型文件。
  • MDL支持利用iOS系統的Matal框架進行GPU加速,能夠顯著提升在iPhone上的運行速度,達到準實時的效果。而NCNN和TFLite還沒有這個功能。

17.8 移動端開源框架部署

17.8.1 以NCNN爲例

部署步驟

17.8.2 以QNNPACK爲例

部署步驟

17.8.4 在Android手機上使用MACE實現圖像分類

17.8.3 在Android手機上使用PaddleMobile實現圖像分類

編譯paddle-mobile庫

1)編譯Android能夠使用的CPP庫:編譯Android的paddle-mobile庫,可選擇使用Docker編譯和Ubuntu交叉編譯,這裏介紹使用Ubuntu交叉編譯paddle-mobile庫。

:在Android項目,Java代碼調用CPP代碼,CPP的函數需要遵循一定的命名規範,比如Java_包名_類名_對應的Java的方法名。

​ 目前官方提供了5個可以給Java調用的函數,該代碼在:paddle-mobile/src/jni/paddle_mobile_jni.cpp,如果想要讓這些函數能夠在自己的包名下的類調用,就要修改CPP的函數名稱修改如下:

JNIEXPORT jboolean JNICALL Java_com_baidu_paddle_PML_load(JNIEnv *env, 
	jclass thiz,
	jstring modelPath) { 
		ANDROIDLOGI("load invoked"); 
		bool optimize = true; 
		return getPaddleMobileInstance()->Load(jstring2cppstring(env, modelPath), optimize); }

​ 筆者項目的包名爲com.example.paddlemobile1,在這個包下有一個ImageRecognition.java的程序來對應這個CPP程序,那麼修改load函數如下:

JNIEXPORT jboolean JNICALL Java_com_example_paddlemobile1_ImageRecognition_load(JNIEnv *env,
                                                          jclass thiz,
                                                          jstring modelPath) {
  ANDROIDLOGI("load invoked");
  bool optimize = true;
  return getPaddleMobileInstance()->Load(jstring2cppstring(env, modelPath),
                                         optimize);
}

使用Ubuntu交叉編譯paddle-mobile庫

1、下載和解壓NDK。

wget https://dl.google.com/android/repository/android-ndk-r17b-linux-x86_64.zip
unzip android-ndk-r17b-linux-x86_64.zip

2、設置NDK環境變量,目錄是NDK的解壓目錄。

export NDK_ROOT="/home/test/paddlepaddle/android-ndk-r17b"

設置好之後,可以使用以下的命令查看配置情況。

root@test:/home/test/paddlepaddle# echo $NDK_ROOT
/home/test/paddlepaddle/android-ndk-r17b

3、安裝cmake,需要安裝較高版本的,筆者的cmake版本是3.11.2。

下載cmake源碼

wget https://cmake.org/files/v3.11/cmake-3.11.2.tar.gz

解壓cmake源碼

tar -zxvf cmake-3.11.2.tar.gz

進入到cmake源碼根目錄,並執行bootstrap。

cd cmake-3.11.2
./bootstrap

最後執行以下兩條命令開始安裝cmake。

make
make install

安裝完成之後,可以使用cmake --version是否安裝成功.

root@test:/home/test/paddlepaddle# cmake --version
cmake version 3.11.2

CMake suite maintained and supported by Kitware (kitware.com/cmake).

4、克隆paddle-mobile源碼。

git clone https://github.com/PaddlePaddle/paddle-mobile.git

5、進入到paddle-mobile的tools目錄下,執行編譯。

cd paddle-mobile/tools/
sh build.sh android

(可選)如果想編譯針對某一個網絡編譯更小的庫時,可以在命令後面加上相應的參數,如下:

sh build.sh android googlenet

6、最後會在paddle-mobile/build/release/arm-v7a/build目錄下生產paddle-mobile庫。

root@test:/home/test/paddlepaddle/paddle-mobile/build/release/arm-v7a/build# ls
libpaddle-mobile.so

libpaddle-mobile.so就是我們在開發Android項目的時候使用到的paddle-mobile庫。

創建Android項目

1、首先使用Android Studio創建一個普通的Android項目,包名爲com.example.paddlemobile1

2、在main目錄下創建l兩個assets/paddle_models文件夾,這個文件夾存放PaddleFluid訓練好的預測模型。PaddleMobile支持量化模型,使用模型量化可以把模型縮小至原來的四分之一,如果使用量化模型,那加載模型的接口也有修改一下,使用以下的接口加載模型:

public static native boolean loadQualified(String modelDir);

3、在main目錄下創建一個jniLibs文件夾,這個文件夾是存放CPP編譯庫的,在本項目中就存放上一部分編譯的libpaddle-mobile.so

4、在Android項目的配置文件夾中加上權限聲明,因爲我們要使用到讀取相冊和使用相機,所以加上以下的權限聲明:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

5、修改activity_main.xml界面,修改成如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
<LinearLayout
    android:id="@+id/btn_ll"
    android:layout_alignParentBottom="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <Button
        android:id="@+id/use_photo"
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="相冊" />

    <Button
        android:id="@+id/start_camera"
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="拍照" />
</LinearLayout>

<TextView
    android:layout_above="@id/btn_ll"
    android:id="@+id/result_text"
    android:textSize="16sp"
    android:layout_width="match_parent"
    android:hint="預測結果會在這裏顯示"
    android:layout_height="100dp" />

<ImageView
    android:layout_alignParentTop="true"
    android:layout_above="@id/result_text"
    android:id="@+id/show_image"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
</RelativeLayout>

6、創建一個ImageRecognition.java的Java程序,這個程序的作用就是調用paddle-mobile/src/jni/paddle_mobile_jni.cpp的函數,對應的是裏面的函數。目前支持一下幾個接口。

package com.example.paddlemobile1;

public class ImageRecognition {
    // set thread num
    public static native void setThread(int threadCount);

//Load seperated parameters
public static native boolean load(String modelDir);

// load qualified model
public static native boolean loadQualified(String modelDir);

// Load combined parameters
public static native boolean loadCombined(String modelPath, String paramPath);

// load qualified model
public static native boolean loadCombinedQualified(String modelPath, String paramPath);

// object detection
public static native float[] predictImage(float[] buf, int[]ddims);

// predict yuv image
public static native float[] predictYuv(byte[] buf, int imgWidth, int imgHeight, int[] ddims, float[]meanValues);

// clear model
public static native void clear();
}

7、然後編寫一個PhotoUtil.java的工具類。

package com.example.paddlemobile1;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

public class PhotoUtil {
// start camera
public static Uri start_camera(Activity activity, int requestCode) {
    Uri imageUri;
    // save image in cache path
    File outputImage = new File(activity.getExternalCacheDir(), "out_image.jpg");
    try {
        if (outputImage.exists()) {
            outputImage.delete();
        }
        outputImage.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
    if (Build.VERSION.SDK_INT >= 24) {
        // compatible with Android 7.0 or over
        imageUri = FileProvider.getUriForFile(activity,
                "com.example.paddlemobile1", outputImage);
    } else {
        imageUri = Uri.fromFile(outputImage);
    }
    // set system camera Action
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // set save photo path
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
    // set photo quality, min is 0, max is 1
    intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
    activity.startActivityForResult(intent, requestCode);
    return imageUri;
}

// get picture in photo
public static void use_photo(Activity activity, int requestCode){
    Intent intent = new Intent(Intent.ACTION_PICK);
    intent.setType("image/*");
    activity.startActivityForResult(intent, requestCode);
}

// get photo from Uri
public static String get_path_from_URI(Context context, Uri uri) {
    String result;
    Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
    if (cursor == null) {
        result = uri.getPath();
    } else {
        cursor.moveToFirst();
        int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
        result = cursor.getString(idx);
        cursor.close();
    }
    return result;
}

// Compress the image to the size of the training image,and change RGB
public static float[] getScaledMatrix(Bitmap bitmap, int desWidth,
                               int desHeight) {
    float[] dataBuf = new float[3 * desWidth * desHeight];
    int rIndex;
    int gIndex;
    int bIndex;
    int[] pixels = new int[desWidth * desHeight];
    Bitmap bm = Bitmap.createScaledBitmap(bitmap, desWidth, desHeight, false);
    bm.getPixels(pixels, 0, desWidth, 0, 0, desWidth, desHeight);
    int j = 0;
    int k = 0;
    for (int i = 0; i < pixels.length; i++) {
        int clr = pixels[i];
        j = i / desHeight;
        k = i % desWidth;
        rIndex = j * desWidth + k;
        gIndex = rIndex + desHeight * desWidth;
        bIndex = gIndex + desHeight * desWidth;
        dataBuf[rIndex] = (float) ((clr & 0x00ff0000) >> 16) - 148;
        dataBuf[gIndex] = (float) ((clr & 0x0000ff00) >> 8) - 148;
        dataBuf[bIndex] = (float) ((clr & 0x000000ff)) - 148;

    }
    if (bm.isRecycled()) {
        bm.recycle();
    }
    return dataBuf;
}

// compress picture
public static Bitmap getScaleBitmap(String filePath) {
    BitmapFactory.Options opt = new BitmapFactory.Options();
    opt.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filePath, opt);

    int bmpWidth = opt.outWidth;
    int bmpHeight = opt.outHeight;

    int maxSize = 500;

    // compress picture with inSampleSize
    opt.inSampleSize = 1;
    while (true) {
        if (bmpWidth / opt.inSampleSize < maxSize || bmpHeight / opt.inSampleSize < maxSize) {
            break;
        }
        opt.inSampleSize *= 2;
    }
    opt.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(filePath, opt);
}
}
  • start_camera()方法是啓動相機並返回圖片的URI。
  • use_photo()方法是打開相冊,獲取到的圖片URI在回到函數中獲取。
  • get_path_from_URI()方法是把圖片的URI轉換成絕對路徑。
  • getScaledMatrix()方法是把圖片壓縮成跟訓練時的大小,並轉換成預測需要用的數據格式浮點數組。
  • getScaleBitmap()方法是對圖片進行等比例壓縮,減少內存的支出。

8、最後修改MainActivity.java,修改如下:

package com.example.paddlemobile1;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getName();
    private static final int USE_PHOTO = 1001;
    private static final int START_CAMERA = 1002;
    private Uri image_uri;
    private ImageView show_image;
    private TextView result_text;
    private String assets_path = "paddle_models";
    private boolean load_result = false;
    private int[] ddims = {1, 3, 224, 224};
private static final String[] PADDLE_MODEL = {
        "lenet",
        "alexnet",
        "vgg16",
        "resnet",
        "googlenet",
        "mobilenet_v1",
        "mobilenet_v2",
        "inception_v1",
        "inception_v2",
        "squeezenet"
};

// load paddle-mobile api
static {
    try {
        System.loadLibrary("paddle-mobile");

    } catch (SecurityException e) {
        e.printStackTrace();

    } catch (UnsatisfiedLinkError e) {
        e.printStackTrace();

    } catch (NullPointerException e) {
        e.printStackTrace();

    }

}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    init();
}

// initialize view
private void init() {
    request_permissions();
    show_image = (ImageView) findViewById(R.id.show_image);
    result_text = (TextView) findViewById(R.id.result_text);
    Button use_photo = (Button) findViewById(R.id.use_photo);
    Button start_photo = (Button) findViewById(R.id.start_camera);

    // use photo click
    use_photo.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            PhotoUtil.use_photo(MainActivity.this, USE_PHOTO);
            //                load_model();
            }
        });
    // start camera click
    start_photo.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            image_uri = PhotoUtil.start_camera(MainActivity.this, START_CAMERA);
        }
    });

    // copy file from assets to sdcard
    String sdcard_path = Environment.getExternalStorageDirectory()
            + File.separator + assets_path;
    copy_file_from_asset(this, assets_path, sdcard_path);

    // load model
    load_model();
}

// load infer model
private void load_model() {
    String model_path = Environment.getExternalStorageDirectory()
            + File.separator + assets_path + File.separator + PADDLE_MODEL[4];
    Log.d(TAG, model_path);
    load_result = ImageRecognition.load(model_path);
    if (load_result) {
        Log.d(TAG, "model load success");
    } else {
        Log.d(TAG, "model load fail");
    }
}

// clear infer model
private void clear_model() {
    ImageRecognition.clear();
    Log.d(TAG, "model is clear");
}

// copy file from asset to sdcard
public void copy_file_from_asset(Context context, String oldPath, String newPath) {
    try {
        String[] fileNames = context.getAssets().list(oldPath);
        if (fileNames.length > 0) {
            // directory
            File file = new File(newPath);
            if (!file.exists()) {
                file.mkdirs();
            }
            // copy recursivelyC
            for (String fileName : fileNames) {
                copy_file_from_asset(context, oldPath + "/" + fileName, newPath + "/" + fileName);
            }
            Log.d(TAG, "copy files finish");
        } else {
            // file
            File file = new File(newPath);
            // if file exists will never copy
            if (file.exists()) {
                return;
            }

            // copy file to new path
            InputStream is = context.getAssets().open(oldPath);
            FileOutputStream fos = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int byteCount;
            while ((byteCount = is.read(buffer)) != -1) {
                fos.write(buffer, 0, byteCount);
            }
            fos.flush();
            is.close();
            fos.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    String image_path;
    RequestOptions options = new RequestOptions().skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE);
    if (resultCode == Activity.RESULT_OK) {
        switch (requestCode) {
            case USE_PHOTO:
                if (data == null) {
                    Log.w(TAG, "user photo data is null");
                    return;
                }
                image_uri = data.getData();
                Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image);
                // get image path from uri
                image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri);
                // show result
                result_text.setText(image_path);
                // predict image
                predict_image(PhotoUtil.get_path_from_URI(MainActivity.this, image_uri));
                break;
            case START_CAMERA:
                // show photo
                Glide.with(MainActivity.this).load(image_uri).apply(options).into(show_image);
                // get image path from uri
                image_path = PhotoUtil.get_path_from_URI(MainActivity.this, image_uri);
                // show result
                result_text.setText(image_path);
                // predict image
                predict_image(PhotoUtil.get_path_from_URI(MainActivity.this, image_uri));
                break;
        }
    }
}

@SuppressLint("SetTextI18n")
private void predict_image(String image_path) {
    // picture to float array
    Bitmap bmp = PhotoUtil.getScaleBitmap(image_path);
    float[] inputData = PhotoUtil.getScaledMatrix(bmp, ddims[2], ddims[3]);
    try {
        long start = System.currentTimeMillis();
        // get predict result
        float[] result = ImageRecognition.predictImage(inputData, ddims);
        Log.d(TAG, "origin predict result:" + Arrays.toString(result));
        long end = System.currentTimeMillis();
        long time = end - start;
        Log.d("result length", String.valueOf(result.length));
        // show predict result and time
        int r = get_max_result(result);
        String show_text = "result:" + r + "\nprobability:" + result[r] + "\ntime:" + time + "ms";
        result_text.setText(show_text);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private int get_max_result(float[] result) {
    float probability = result[0];
    int r = 0;
    for (int i = 0; i < result.length; i++) {
        if (probability < result[i]) {
            probability = result[i];
            r = i;
        }
    }
    return r;
}

// request permissions
private void request_permissions() {

    List<String> permissionList = new ArrayList<>();
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
        permissionList.add(Manifest.permission.CAMERA);
    }

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
    }

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
    }

    // if list is not empty will request permissions
    if (!permissionList.isEmpty()) {
        ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), 1);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0) {
                for (int i = 0; i < grantResults.length; i++) {

                    int grantResult = grantResults[i];
                    if (grantResult == PackageManager.PERMISSION_DENIED) {
                        String s = permissions[i];
                        Toast.makeText(this, s + " permission was denied", Toast.LENGTH_SHORT).show();
                    }
                }
            }
            break;
    }
}

@Override
protected void onDestroy() {
    // clear model before destroy app
    clear_model();
    super.onDestroy();
}
}
  • load_model()方法是加載預測模型的。
  • clear_model()方法是清空預測模型的。
  • copy_file_from_asset()方法是把預測模型複製到內存卡上。
  • predict_image()方法是預測圖片的。
  • get_max_result()方法是獲取概率最大的預測結果。
  • request_permissions()方法是動態請求權限的。

因爲使用到圖像加載框架Glide,所以要在build.gradle加入以下的引用。

implementation 'com.github.bumptech.glide:glide:4.3.1'

8、最後運行項目,選擇圖片預測就會得到結果。

17.9 移動端開源框架部署疑難

增加常見的幾個問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章