TensorFlow 模型 Quantization 離散化或量化

目前我們使用神經網路的時候,通常都是使用浮點數,這是保持模型準確性的最簡單方法,並且GPU可以很好地加速這些計算,所以很自然的是對其他數字格式沒有太多關注。

但是實際使用中,有很多模型被部署在商業應用中,模型的前向推導計算需求隨著用戶數量的增加而成正比,這意味著純粹的前向推理效率已經成為很多團隊的熱門關注點。

這就是 Quantization 之所以出現的原因,它以比32位浮點數更緊湊的格式來存儲數字,並對它們進行計算。本文重點放在8位固定點數值格式上,原因在後面我會詳細討論。

為什麼 Quantization 可以進行?

深度網路的神奇特性之一是它們傾向於很好地處理輸入中的高度雜訊。如果您想要識別您拍攝的照片中的物體,網路必須忽略所有噪音,光線變化以及其與之前所見的訓練樣例之間的其他非本質區別,並將重點放在重要的相似之處。這種能力意味著他們似乎將低精度計算看作是雜訊的另一個來源,即使8位的數字格式保存的信息較少,仍然可以產生準確的結果。

為什麼要 Quantization 量化?

第一個原因就是減小模型佔用空間,例如,神經網路模型可佔用大量磁碟空間,原始AlexNet的浮點格式超過200 MB。幾乎所有的大小都被神經連接的權重所佔據,因為在單個模型中經常有數百萬個權重參數,並且它們都是略有不同的浮點數,所以像zip這樣簡單的壓縮格式並不能很好地壓縮它們。並且它們有一個較好的特徵,那就是在每一層內的權重傾向於在一定範圍內正態分布,例如-3.0至6.0。

量化最簡單的方式是通過先存儲每個層的最小值和最大值,然後將每個浮點值壓縮成一個8位整數,在最大值、最小值範圍內空間線性劃分 256 段,每段用一個唯一的 8-bit 整數表示在該段內的實數值; 例如-3.0到6.0的範圍,0位元組將代表-3.0,255代表6.0,128代表約1.5。簡單的使用8位整數存儲權重就意味著可以從磁碟上壓縮大約 75% 的文件大小,然後在載入後轉換回浮點數,以便現有的浮點代碼可以沒有任何改變地工作。

另一個原因是如果不像上述方式只是使用8位整數來存儲模型,而是完全用8位整數執行輸入和輸出,以及執行推理計算,可以大大減少計算資源;但這樣做要困難得多,因為需要改變模型的所有代碼,但是這樣做有很多好處。獲取8位值只需要浮點內存帶寬的25%,因此可以更好地使用緩存並避免RAM訪問出現瓶頸,也可以在每個時鐘周期執行更多的SIMD操作。另外如果有一個可以加速8位計算的DSP晶元,將可以提供更多優勢。

將計算結果移至8位可幫助您更快速地運行模型,並使用更少的功耗(這對於移動設備尤為重要),它也為許多無法高效運行浮點代碼的嵌入式系統打開了大門,因此可以在物聯網設備中啟用大量的應用程序。

為什麼不直接用8位的權重進行較低的精度訓練?

一些實驗結果表明,必須使用高於8比特的數據來處理反向傳播和梯度的數值。並且我們已經有很多可以使用的浮點數模型,所以可以非常方便的直接轉換它們。

如何 Quantization 量化模型?

TensorFlow擁有內置8位計算的支持,它可將許多經過浮點數值訓練的模型轉換為同等的計算圖,並使用離散化的計算進行前向推理。下面是如何將GoogLeNet模型轉換為使用8位計算的版本:

curl -L "https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz" | tar -C tensorflow/examples/label_image/data -xzbazel build tensorflow/tools/graph_transforms:transform_graphbazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=tensorflow/examples/label_image/data/inception_v3_2016_08_28_frozen.pb --out_graph=/tmp/quantized_graph.pb --inputs=input --outputs=InceptionV3/Predictions/Reshape_1 --transforms=add_default_attributes strip_unused_nodes(type=float, shape="1,299,299,3") remove_nodes(op=Identity, op=CheckNumerics) fold_constants(ignore_errors=true) fold_batch_norms fold_old_batch_norms quantize_weights quantize_nodes strip_unused_nodes sort_by_execution_order

上面的代碼使用 bazel 工具,把原有的 inception_v3_2016_08_28_froze.pb 模型進行了轉換, 然後產生一個新的模型 quantized_graph.pb,注意它指定的幾個參數,原有模型、轉換後的模型、輸入、輸出、轉換過程的屬性(其中有 quantize_weights 和 quantize_nodes )

它的計算過程與原來的相同,但內部執行8位計算,所有的權重和節點 8 位量化。如果你看一下文件大小,你會發現它大約只是原來的四分之一(23MB和91MB),並且仍然可以使用完全相同的輸入和輸出來運行此模型,最後應該可以得到相同的結果;這是一個例子:

bazel build tensorflow/examples/label_image:label_imagebazel-bin /tensorflow/examples/label_image/label_image --image=<input-image> --graph=/tmp/quantized_graph.pb --labels=/tmp/imagenet_synset_to_human_label_map.txt --input_width_=299 --input_height=299 --input_mean=128 --input_std=128 --input_layer="Mul:0" --output_layer="softmax:0"

這個運行會輸出一個和原模型非常類似的結果。

也可以在 ckpt 保存的模型上運行相同的流程,輸入和輸出名稱需要根據網路的具體情況進行調整,建議先通過freeze_graph腳本運行它們,把 ckpt 的文件轉換成pb 文件。

Quantization 量化過程在模型內部的計算操作上如何進行?

通過編寫等價的前向推導時常用的 8-bit 版本運算符實現了量化計算,這些運算符包括卷積、矩陣乘、激活函數、下採樣和拼接。轉換腳本首先替換所有已知的運算符為等價的量化運算符。

以 relu 計算為例,首先是原始的Relu操作,帶有浮點輸入和輸出:

Relu圖

然後,這是等價的轉換子圖,仍然具有浮點輸入和輸出,但是具有內部轉換,所以計算在8位完成。

轉換圖

從這個示意圖可以看出,這實際上就是把 relu 的中間操作替換了,先做數值映射到8位的過程,然後進行8位的 relu 計算,最後執行Dequantize操作,得到的結果就和原始的 relu 一樣了。

當單個運算操作被轉換之後,下一個階段是消除不必要的浮點數到8位數值的轉換。如果連續的計算序列都存在這種 Dequantize / Quantize 的操作 ,那麼將會有很多相鄰的 Dequantize / Quantize操作,而這是可以抵消的,所以可以在這個階段發現這種相鄰的Dequantize / Quantize 模式,認識到他們可以相互抵消,並消除他們,就像下圖這樣,左右兩邊是等價的,可以去掉多餘的Dequantize / Quantize操作:

目前所有運算符都有量化版本,可以實現一張全部計算都以 8-bit 實現的計算圖Graph,無需進行轉換回浮點的操作。

不過有個地方有個疑問,如果是存在連續的多個計算操作,它們都需要使用Quantization操作,但是其實每個映射步驟的最大值和最小值是不一樣的,如何保證這樣的抵消操作是等價並且可行的? 只有在幾個步驟的最大值最小值是比較接近的情況下才可以做這樣的抵消操作吧?

怎麼使用 Quantized 量化表示權重?

首先用兩個浮點數存儲最低和最高量化值表示權重的整體最小值和最大值,然後量化數組中的每個數值表示該範圍內的浮點值,它們在最小值和最大值之間線性分布。例如,如果我們有最小值= -10.0,最大值= 30.0f,和一個8位數組,這裡是量化值表示:

量化值 | 浮點數------ | -----0 | -10.0255 | 30.0128 | 10.0

這種格式的優點是它可以表示任意的浮點數範圍,浮點數分布不必是對稱的,可以表示有符號和無符號的值,並且線性的映射可以直接進行乘法運算。也有其他的替代方案,但是這些計算的代價往往比較昂貴。

通過論文對權重量化壓縮和激活函數量化的效果列一個有比較明顯的直觀感受的實驗結果,AlexNet模型 前向推導 forward 結果對比:

其中 BW 表示權重的量化壓縮,Q 表示作者提出的一個激活函數的量化 quantization function. Q(x),sign 是通常使用的激活函數的量化的表示,可以看出:

  • Fullweight和Binary weight對比來看,差距在3%左右,差距不是很大,這個結果驗證了weight的壓縮效果
  • 作者自己提出的 Q 激活函數的量化效果明顯好於傳統的 sign
  • 使用浮點數權重加上激活函數的量化後,模型效果下降為6%-20%,下降較多,這樣來看,激活函數的量化效果確實沒有權重的量化效果好

所以總結起來,這個量化的操作實際上輸入輸出依舊是float,只不過中間的計算是用過8 bit來計算的,它就是把權重以及內部的節點運算都轉化成使用8位的數值來進行計算和存儲,從而達到模型壓縮和模型加速的目的。

參考資料:

How to Quantize Neural Networks with TensorFlow | TensorFlow

Deep Learning with Low Precision by Half-wave Gaussian Quantization,arxiv.org/abs/1702.0095


推薦閱讀:

CNN中卷積層的計算細節
配置深度學習主機與環境(TensorFlow+1080Ti) | 第一章:硬體選購與主機組裝

TAG:TensorFlow | 深度学习DeepLearning |