模型量化詳解

1、模型量化是什麼?

  簡而言之,所謂的模型量化就是將浮點存儲(運算)轉換爲整型存儲(運算)的一種模型壓縮技術。簡單直白點講,即原來表示一個權重需要使用float32表示,量化後只需要使用int8來表示就可以啦,僅僅這一個操作,我們就可以獲得接近4倍的網絡加速

2、爲什麼需要做模型量化?

  隨着深度學習技術在多個領域的快速應用,具體包括計算機視覺-CV、自然語言處理-NLP、語音等,出現了大量的基於深度學習的網絡模型。這些模型都有一個特點,即大而複雜、適合在N卡上面進行推理,並不適合應用在手機等嵌入式設備中,而客戶們通常需要將這些複雜的模型部署在一些低成本的嵌入式設備中,因而這就產生了一個矛盾。爲了很好的解決這個矛盾,模型量化應運而生,它可以在損失少量精度的前提下對模型進行壓縮,使得將這些複雜的模型應用到手機、機器人等嵌入式終端中變成了可能
  隨着模型預測越來越準確,網絡越來越深,神經網絡消耗的內存大小成爲一個核心的問題,尤其是在移動設備上。通常情況下,目前的手機一般配備 4GB 內存來支持多個應用程序的同時運行,而三個模型運行一次通常就要佔用1GB內存。
  模型大小不僅是內存容量問題,也是內存帶寬問題。模型在每次預測時都會使用模型的權重,圖像相關的應用程序通常需要實時處理數據,這意味着至少 30 FPS。因此,如果部署相對較小的 ResNet-50 網絡來分類,運行網絡模型就需要 3GB/s 的內存帶寬。網絡運行時,內存,CPU 和電池會都在飛速消耗,我們無法爲了讓設備變得智能一點點就負擔如此昂貴的代價。

3、模型量化動機是什麼?

  • 更少的存儲開銷和帶寬需求。即使用更少的比特數存儲數據,有效減少應用對存儲資源的依賴,但現代系統往往擁有相對豐富的存儲資源,這一點已經不算是採用量化的主要動機;

  • 更快的計算速度。即對大多數處理器而言,整型運算的速度一般(但不總是)要比浮點運算更快一些;

  • 更低的能耗與佔用面積
    在這裏插入圖片描述
      從上圖中可以看到,FP32乘法運算的能耗是INT8乘法運算能耗的18.5倍,芯片佔用面積則是int8的27.3倍,而對於芯片設計和FPGA設計而言,更少的資源佔用意味着相同數量的單元下可以設計出更多的計算單元;而更少的能耗意味着更少的發熱,和更長久的續航。

  • 尚可接受的精度損失即量化相當於對模型權重引入噪聲,所幸CNN本身對噪聲不敏感(在模型訓練過程中,模擬量化所引入的權重加噪還有利於防止過擬合),在合適的比特數下量化後的模型並不會帶來很嚴重的精度損失。按照gluoncv提供的報告,經過int8量化之後,ResNet50_v1和MobileNet1.0 _v1在ILSVRC2012數據集上的準確率僅分別從77.36%、73.28%下降爲76.86%、72.85%。

  • 支持int8是一個大的趨勢。即無論是移動端還是服務器端,都可以看到新的計算設備正不斷迎合量化技術。比如NPU/APU/AIPU等基本都是支持int8(甚至更低精度的int4)計算的,並且有相當可觀的TOPs,而Mali GPU開始引入int8 dot支持,Nvidia也不例外。除此之外,當前很多創業公司新發布的邊緣端芯片幾乎都支持int8類型。

4、模型量化分類

根據映射函數是否是線性可以分爲兩類-即線性量化和非線性量化,本文主要研究的是線性量化技術。

4.1 線性量化

  常見的線性量化過程可以用以下數學表達式來表示:r=Round(S(qZ))r=\operatorname{Round}(S(q-Z))
其中,q 表示的是原始的float32數值;
Z表示的是float32數值的偏移量,在很多地方又叫Zero Point;
S表示的是float32的縮放因子,在很多地方又叫Scale;
Round(⋅) 表示的是四捨五入近似取整的數學函數,除了四捨五入,使用向上或者向下取整也是可以的;
r表示的是量化後的一個整數值。

根據參數 Z 是否爲零可以將線性量化分爲兩類—即對稱量化和非對稱量化

4.1.1 對稱量化

在這裏插入圖片描述
  如上圖所示,所謂的對稱量化,即使用一個映射公式將輸入數據映射到[-128,127]的範圍內,圖中-max(|Xf|)表示的是輸入數據的最小值,max(|Xf|)表示輸入數據的最大值。對稱量化的一個核心即零點的處理,映射公式需要保證原始的輸入數據中的零點通過映射公式後仍然對應[-128,127]區間的零點。總而言之,對稱量化通過映射關係將輸入數據映射在[-128,127]的範圍內,對於映射關係而言,我們需要求解的參數即Z和S。
  在對稱量化中,r 是用有符號的整型數值(int8)來表示的,此時 Z=0,且 q=0時恰好有r=0。在對稱量化中,我們可以取Z=0,S的取值可以使用如下的公式,也可以採用其它的公式。
S=2n11max(x) S=\frac{2^{n-1}-1}{\max (|x|)}
其中,n 是用來表示該數值的位寬,x 是數據集的總體樣本。

4.1.2 非對稱量化

在這裏插入圖片描述
  如上圖所示,所謂的非對稱量化,即使用一個映射公式將輸入數據映射到[0,255]的範圍內,圖中min(Xf)表示的是輸入數據的最小值,max(Xf)表示輸入數據的最大值。總而言之,對稱量化通過映射關係將輸入數據映射在[0,255]的範圍內,對於映射關係而言,我們需要求解的參數即Z和S。
  在非對稱量化中,r 是用有符號的整型數值(uint8)來表示的。在非對稱量化中,我們可以取Z=min(x),S的取值可以使用如下的公式,也可以採用其它的公式。
S=2n11max(x)min(x) S=\frac{2^{n-1}-1}{\max (x)-\min (x)}

4.2 逐層量化、逐組量化和逐通道量化

根據量化的粒度(共享量化參數的範圍)可以分爲逐層量化、逐組量化和逐通道量化。

  • 逐層量化以一個層爲單位,整個layer的權重共用一組縮放因子S和偏移量Z
  • 逐組量化以組爲單位,每個group使用一組S和Z
  • 逐通道量化則以通道爲單位,每個channel單獨使用一組S和Z

當 group=1 時,逐組量化與逐層量化等價;當 group=num_filters (即dw卷積)時,逐組量化逐通道量化等價。

4.3 在線量化和離線量化

根據激活值的量化方式,可以分爲在線(online)量化和離線(offline)量化。

  • 在線量化,即指激活值的S和Z在實際推斷過程中根據實際的激活值動態計算
  • 離線量化,即指提前確定好激活值的S和Z;

  由於不需要動態計算量化參數,通常離線量化的推斷速度更快些,通常通過以下的三種方法來確定相關的量化參數。

  • 指數平滑法。即將校準數據集送入模型,收集每個量化層的輸出特徵圖,計算每個batch的S和Z值,並通過指數平滑法來更新S和Z值
  • 直方圖截斷法。即在計算量化參數Z和S的過程中,由於有的特徵圖會出現偏離較遠的奇異值,導致max非常大,所以可以通過直方圖截取的形式,比如拋棄最大的前1%數據,以前1%分界點的數值作爲max計算量化參數
  • KL散度校準法。-即通過計算KL散度(也稱爲相對熵,用以描述兩個分佈之間的差異)來評估量化前後的兩個分佈之間存在的差異,搜索並選取KL散度最小的量化參數Z和S作爲最終的結果。Tensorflow中就採用了這種方法。

4.4 比特量化

根據存儲一個權重元素所需的位數,可以將其分爲8bit量化、4bit量化、2bit量化和1bit量化等。

  • 二進制神經網絡。即在運行時具有二進制權重和激活的神經網絡,以及在訓練時計算參數的梯度。
  • 三元權重網絡。即權重約束爲+1,0和-1的神經網絡。
  • XNOR網絡。即過濾器和卷積層的輸入是二進制的。XNOR 網絡主要使用二進制運算來近似卷積。

4.5 權重量化和權重激活量化

根據需要量化的參數可以分類兩類-權重量化和權重激活量化

  • 權重量化,即僅僅需要對網絡中的權重執行量化操作。由於網絡的權重一般都保存下來了,因而我們可以提前根據權重獲得相應的量化參數S和Z。由於僅僅對權重執行了量化,這種量化方法的壓縮力度不是很大。
  • 權重激活量化,即不僅對網絡中的權重進行量化,還對激活值進行量化。由於激活層的範圍通常不容易提前獲得,因而需要在網絡推理的過程中進行計算或者根據模型進行大致的預測。

5、模型量化原理詳解

5.1 原理詳解

  模型量化橋接了定點與浮點,建立了一種有效的數據映射關係,使得以較小的精度損失代價獲得了較好的收益,要弄懂模型量化的原理就是要弄懂這種數據映射關係。
浮點轉換爲定點的公式如下所示:Q=RS+Z Q=\frac{R}{S}+Z
定點轉換爲浮點的公式如下所示:R=(QZ)S R=(Q-Z) * S
其中R表示輸入的浮點數據,Q表示量化之後的定點數據,Z表示Zero Point的數值,S表示Scale的數值,我們可以根據S和Z這兩個參數來確定這個映射關係。求解S和Z有很多種方法,這裏列舉中其中的一種求解方式如下:
S=RmaxRminQmaxQmin S=\frac{R_{\max }-R_{\min }}{Q_{\max }-Q_{\min }}
其中RmaxR_{\max }表示輸入浮點數據中的最大值,RminR_{\min }表示輸入浮點數據中的最小值,QmaxQ_{\max }表示最大的定點值(127/255),QminQ_{\min }表示最小的定點值(-128/0)
Z=QmaxRmax÷S Z=Q_{\max }-R_{\max } \div S
每通道或每張量的權重用int8進行定點量化的可表示範圍爲[-127,127],且zero-point就是量化值0;
每張量的激活值或輸入值用int8進行定點量化的可表示範圍爲[-128,127],其zero-point在[-128,127]內依據公式求得;

5.2 具體案例

  訓練後的模型權重或激活值往往在一個有限的範圍內分佈,如激活值範圍爲[-2.0, 6.0],然後我們使用int8進行模型量化,則定點量化值範圍爲[-128, 127],那麼S和Z的求值過程如下所示:
S=6.0(2.0)127(128)=8.02550.031372549Z=1276.031372549127191.2564.2564 \begin{array}{l}{S=\frac{6.0-(-2.0)}{127-(-128)}=\frac{8.0}{255} \approx 0.031372549} \\ {Z=127-6.031372549 \approx 127-191.25 \approx-64.25 \approx-64}\end{array}

如果此時我們有一個真實的激活值爲0.28即R=0.28,那麼對應Q的求解過程如下所示:
Q=0.28÷0.031372549+(64)8.9256455.07555 Q=0.28 \div 0.031372549+(-64) \approx 8.925-64 \approx-55.075 \approx-55
整個網絡中的其它參數也按照這種方法就可以獲得量化之後的數值。

6、模型量化實現步驟

  對於模型量化任務而言,具體的執行步驟如下所示:

  • 步驟1-在輸入數據(通常是權重或者激活值)中統計出相應的min_value和max_value;
  • 步驟2-選擇合適的量化類型,對稱量化(int8)還是非對稱量化(uint8);
  • 步驟3-根據量化類型、min_value和max_value來計算獲得量化的參數Z/Zero point和S/Scale
  • 步驟4-根據標定數據對模型執行量化操作,即將其由FP32轉換爲INT8;
  • 步驟5-驗證量化後的模型性能,如果效果不好,嘗試着使用不同的方式計算S和Z,重新執行上面的操作;

7、Pytorch模型量化詳解

7.1 簡介

  具體的細節請參考該鏈接
  量化是指用於執行計算並以低於浮點精度的位寬存儲張量的技術。量化模型對張量使用整數而不是浮點值執行部分或全部運算。這允許更緊湊的模型表示,並在許多硬件平臺上使用高性能矢量化操作。與典型的FP32型號相比,PyTorch支持INT8量化,從而可將模型尺寸減少4倍,並將內存帶寬要求減少4倍。與FP32計算相比,對INT8計算的硬件支持通常快2到4倍。量化主要是一種加速推理的技術,並且量化算子僅支持前向傳遞。
  PyTorch支持多種量化深度學習模型的方法。在大多數情況下,該模型在FP32中訓練,然後將模型轉換爲INT8。此外,PyTorch還支持訓練時量化,該訓練使用僞量化模塊對前向和後向傳遞中的量化誤差進行建模。注意,整個計算是在浮點數中進行的。在量化意識訓練結束時,PyTorch提供轉換功能,將訓練後的模型轉換爲較低的精度。
  PyTorch支持每個張量和每個通道非對稱線性量化。每個張量意味着張量內的所有值都以相同的方式縮放。每通道意味着對於每個尺寸(通常是張量的通道尺寸),張量中的值都按比例縮放並偏移一個不同的值(實際上,比例和偏移成爲矢量)。這樣可以在將張量轉換爲量化值時減少誤差。爲了在PyTorch中進行量化,我們需要能夠以張量表示量化數據。量化張量允許存儲量化數據(表示爲int8 / uint8 / int32)以及諸如scale和zero_point之類的量化參數。量化張量除了允許以量化格式序列化數據外,還允許許多有用的操作使量化算術變得容易。

7.2 pytorch量化工作流程詳解

  PyTorch提供了三種量化模型的方法,具體包括訓練後動態量化、訓練後靜態量化和訓練時量化

  • 訓練後動態量化。這是最簡單的量化形式,其中權重被提前量化,而激活在推理過程中被動態量化。這種方法用於模型執行時間由從內存加載權重而不是計算矩陣乘法所支配的情況,這適用於批量較小的LSTM和Transformer類型。對整個模型應用動態量化只需要調用一次torch.quantization.quantize_dynamic()函數即可完成具體的細節請參考該量化教程
  • 訓練後靜態量化。這是最常用的量化形式,其中權重是提前量化的,並且基於在校準過程中觀察模型的行爲來預先計算激活張量的比例因子和偏差。CNN是一個典型的用例,訓練後量化通常是在內存帶寬和計算節省都很重要的情況下進行的。進行訓練後量化的一般過程如下所示:
    步驟1-準備模型:通過添加QuantStub和DeQuantStub模塊,指定在何處顯式量化和反量化激活值;確保不重複使用模塊;將需要重新量化的任何操作轉換爲模塊的模式;
    步驟2-將諸如conv + relu或conv + batchnorm + relu之類的組合操作融合在一起,以提高模型的準確性和性能;
    步驟3-指定量化方法的配置,例如選擇對稱或非對稱量化以及MinMax或L2Norm校準技術;
    步驟4- 插入torch.quantization.prepare()模塊來在校準期間觀察激活張量
    步驟5-使用校準數據集對模型執行校準操作
    步驟6-使用torch.quantization.convert() 模塊來轉化模型,具體包括計算並存儲每個激活張量要使用的比例和偏差值,並替換關鍵算子的量化實現等。
  • 訓練時量化。在極少數情況下,訓練後量化不能提供足夠的準確性,可以插入torch.quantization.FakeQuantize()模塊執行訓練時量化。計算將在FP32中進行,但將值取整並四捨五入以模擬INT8的量化效果。具體的量化步驟如下所示:
    步驟1-準備模型:通過添加QuantStub和DeQuantStub模塊,指定在何處顯式量化和反量化激活值;確保不重複使用模塊;將需要重新量化的任何操作轉換爲模塊的模式;
    步驟2-將諸如conv + relu或conv + batchnorm + relu之類的組合操作融合在一起,以提高模型的準確性和性能;
    步驟3-指定僞量化方法的配置,例如選擇對稱或非對稱量化以及MinMax或L2Norm校準技術;
    步驟4-插入torch.quantization.prepare_qat() 模塊,該模塊用來在訓練過程中的模擬量化;
    步驟5-訓練或者微調模型
    步驟6-使用torch.quantization.convert() 模塊來轉化模型,具體包括計算並存儲每個激活張量要使用的比例和偏差值,並替換關鍵算子的量化實現等。

7.3 Pytorch模型量化代碼實戰

# 導入第三方的庫函數
import os
from io import open
import time

import torch
import torch.nn as nn
import torch.quantization
import torch.nn.functional as F

# 創建LSTM模型類
class LSTMModel(nn.Module):
    """整個網絡包含一個encoder, 一個recurrent模塊和一個decoder."""

    def __init__(self, ntoken, ninp, nhid, nlayers, dropout=0.5):
        super(LSTMModel, self).__init__()
        # 預定義一些網絡層
        self.drop = nn.Dropout(dropout)
        self.encoder = nn.Embedding(ntoken, ninp)
        self.rnn = nn.LSTM(ninp, nhid, nlayers, dropout=dropout)
        self.decoder = nn.Linear(nhid, ntoken)
        self.init_weights()
        self.nhid = nhid
        self.nlayers = nlayers

    def init_weights(self):
    	'''
    	初始化模型權重
    	'''
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange, initrange)
        self.decoder.bias.data.zero_()
        self.decoder.weight.data.uniform_(-initrange, initrange)

    def forward(self, input, hidden):
    	'''
    	搭建網絡並執行前向推理
    	'''
        emb = self.drop(self.encoder(input))
        output, hidden = self.rnn(emb, hidden)
        output = self.drop(output)
        decoded = self.decoder(output)
        return decoded, hidden

    def init_hidden(self, bsz):
    	'''
    	初始化hidden層的權重
    	'''
        weight = next(self.parameters())
        return (weight.new_zeros(self.nlayers, bsz, self.nhid),
                weight.new_zeros(self.nlayers, bsz, self.nhid))

# 創建一個詞典類,用來處理數據
class Dictionary(object):
    def __init__(self):
        self.word2idx = {}
        self.idx2word = []

    def add_word(self, word):
    	'''
    	在詞典中添加新的word
    	'''
        if word not in self.word2idx:
            self.idx2word.append(word)
            self.word2idx[word] = len(self.idx2word) - 1
        return self.word2idx[word]

    def __len__(self):
    	'''
    	返回詞典的長度
    	'''
        return len(self.idx2word)

class Corpus(object):
    def __init__(self, path):
        self.dictionary = Dictionary()
        # 分別獲取訓練集、驗證集和測試集
        self.train = self.tokenize(os.path.join(path, 'train.txt'))
        self.valid = self.tokenize(os.path.join(path, 'valid.txt'))
        self.test = self.tokenize(os.path.join(path, 'test.txt'))

    def tokenize(self, path):
        """對輸入的文件執行分詞操作"""
        assert os.path.exists(path)
        # 將新的單詞添加到詞典中
        with open(path, 'r', encoding="utf8") as f:
            for line in f:
                words = line.split() + ['<eos>']
                for word in words:
                    self.dictionary.add_word(word)

        # 標記文件的內容
        with open(path, 'r', encoding="utf8") as f:
            idss = []
            for line in f:
                words = line.split() + ['<eos>']
                ids = []
                for word in words:
                    ids.append(self.dictionary.word2idx[word])
                idss.append(torch.tensor(ids).type(torch.int64))
            ids = torch.cat(idss)
        return ids

# 設置模型的路徑
model_data_filepath = 'data/'
corpus = Corpus(model_data_filepath + 'wikitext-2')
ntokens = len(corpus.dictionary)

# 搭建網絡模型
model = LSTMModel(
    ntoken = ntokens,
    ninp = 512,
    nhid = 256,
    nlayers = 5,
)

# 裝載預訓練的權重
model.load_state_dict(
    torch.load(
        model_data_filepath + 'word_language_model_quantize.pth',
        map_location=torch.device('cpu')
        )
    )
# 將模型切換爲推理模式,並打印整個模型
model.eval()
print(model)

# 獲取一個隨機的輸入數值
input_ = torch.randint(ntokens, (1, 1), dtype=torch.long)
hidden = model.init_hidden(1)
temperature = 1.0
num_words = 1000

# 遍歷數據集進行前向推理並將結果保存起來
with open(model_data_filepath + 'out.txt', 'w') as outf:
    with torch.no_grad():  # no tracking history
        for i in range(num_words):
            output, hidden = model(input_, hidden)
            word_weights = output.squeeze().div(temperature).exp().cpu()
            word_idx = torch.multinomial(word_weights, 1)[0]
            input_.fill_(word_idx)
            word = corpus.dictionary.idx2word[word_idx]
            outf.write(str(word.encode('utf-8')) + ('\n' if i % 20 == 19 else ' '))
            if i % 100 == 0:
                print('| Generated {}/{} words'.format(i, 1000))

with open(model_data_filepath + 'out.txt', 'r') as outf:
    all_output = outf.read()
    print(all_output)

bptt = 25
criterion = nn.CrossEntropyLoss()
eval_batch_size = 1

# 創建測試數據集
def batchify(data, bsz):
    # 對測試數據集進行分塊
    nbatch = data.size(0) // bsz
    # 去掉多餘的元素
    data = data.narrow(0, 0, nbatch * bsz)
    # 在bsz批處理中平均劃分數據
    return data.view(bsz, -1).t().contiguous()

test_data = batchify(corpus.test, eval_batch_size)

# 獲取bath塊的輸入數據
def get_batch(source, i):
    seq_len = min(bptt, len(source) - 1 - i)
    data = source[i:i+seq_len]
    target = source[i+1:i+1+seq_len].view(-1)
    return data, target

def repackage_hidden(h):
  """
  用新的張量把隱藏的狀態包裝起來,把它們從歷史中分離出來
  """
  if isinstance(h, torch.Tensor):
      return h.detach()
  else:
      return tuple(repackage_hidden(v) for v in h)
# 評估函數
def evaluate(model_, data_source):
    # 打開評估模式
    model_.eval()
    total_loss = 0.
    hidden = model_.init_hidden(eval_batch_size)
    with torch.no_grad():
        for i in range(0, data_source.size(0) - 1, bptt):
        	# 獲取測試數據
            data, targets = get_batch(data_source, i)
            # 執行前向推理
            output, hidden = model_(data, hidden)
            hidden = repackage_hidden(hidden)
            output_flat = output.view(-1, ntokens)
            # 獲取訓練loss
            total_loss += len(data) * criterion(output_flat, targets).item()
    return total_loss / (len(data_source) - 1)

# 初始化動態量化模塊
quantized_model = torch.quantization.quantize_dynamic(
    model, {nn.LSTM, nn.Linear}, dtype=torch.qint8
)
print(quantized_model)

def print_size_of_model(model):
    torch.save(model.state_dict(), "temp.p")
    print('Size (MB):', os.path.getsize("temp.p")/1e6)
    os.remove('temp.p')

print_size_of_model(model)
print_size_of_model(quantized_model)

torch.set_num_threads(1)
# 評估模型的運行時間
def time_model_evaluation(model, test_data):
    s = time.time()
    loss = evaluate(model, test_data)
    elapsed = time.time() - s
    print('''loss: {0:.3f}\nelapsed time (seconds): {1:.1f}'''.format(loss, elapsed))

time_model_evaluation(model, test_data)
time_model_evaluation(quantized_model, test_data)

8、Tensorflow模型量化詳解

8.1 簡介

  具體的細節請參考該鏈接
  Tensorflow Lite 和 Tensorflow Model Optimization Toolkit (Tensorflow模型優化工具包)提供了最小優化推理複雜性的工具。對於移動和物聯網 (IoT) 等邊緣設備,推理效率尤其重要。這些設備在處理,內存,能耗和模型存儲方面有許多限制。 此外,模型優化解鎖了定點硬件 (fixed-point hardware) 和下一代硬件加速器的處理能力。
  深度神經網絡的量化使用了一些技術,這些技術可以降低權重的精確表示,並且可選的降低存儲和計算的激活值。量化的優點包括:

  • 1、對現有 CPU 平臺的支持。
  • 2、激活值得的量化降低了用於讀取和存儲中間激活值的存儲器訪問成本。
  • 3、許多 CPU 和硬件加速器實現提供 SIMD 指令功能,這對量化特別有益。

TensorFlow Lite 對量化提供了多種級別的對量化支持。

  • Tensorflow Lite post-training quantization 量化使權重和激活值的 Post training 更簡單
  • Quantization-aware training 可以以最小精度下降來訓練網絡;這僅適用於卷積神經網絡的一個子集

8.2 tensorflow訓練後量化詳解

  tensorflow訓練後量化是針對已訓練好的模型而言的,針對大部分我們已訓練好的的網絡模型來說均可使用此方法進行模型量化。tensorflow提供了一整套完整的模型量化工具,如TensorFlow Lite Optimizing COnverter(toco命令工具)以及TensorFlow Lite converter(API源碼調用接口)

8.2.1 混合量化-僅量化權重

  該方式將浮點型的權重量化爲int8整型,可將模型大小直接減少75%、提升推理速度最大3倍。該方式在推理的過程中,需要將int8量化值反量化爲浮點型後再進行計算,如果某些Ops不支持int8整型量化,那麼其保存的權重依然是浮點型的,即部分支持int8量化的Ops其權重保存爲int8整型且存在quantize和dequantize操作,否則依然是浮點型的,因而稱該方式爲混合量化。該方式可達到近乎全整型量化的效果,但存在quantize和dequantize操作其速度依然不夠理想。
混合量化的實現方式比較簡單,僅需調用tf.lite.TFLiteConverter的API轉化即可

import tensorflow as tf
# 裝載預訓練模型
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
# 設置優化器
converter.optimizations = [tf.lite.Optimize.OPTIMIZE_FOR_SIZE]
# 執行轉換操作
tflite_quant_model = converter.convert()

8.2.2 全整型量化-權重和激活值都進行量化

  該方式試圖將權重、激活值及輸入值均全部做int8量化,並且將所有模型運算操作置於int8下進行執行,以達到最好的量化效果。爲了達到此目的,我們需要一個具有代表性的小數據集,用於統計激活值和輸入值等的浮點型範圍,以便進行精準量化。
  全整型量化的輸入輸出依然是浮點型的,但如果某些Ops未實現該方法,則轉化是沒問題的且其依然會自動保存爲浮點型,這就要求我們的硬件支持這樣的操作。

import tensorflow as tf

def representative_dataset_gen():
  for _ in range(num_calibration_steps):
    # Get sample input data as a numpy array in a method of your choosing.
    yield [input]

# 裝載預訓練模型
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
# 設置優化器
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 獲得標註數據
converter.representative_dataset = representative_dataset_gen
# 執行轉化操作
tflite_quant_model = converter.convert()

8.2.3 半精度量化-僅量化權重

  該方式是將權重量化爲半精度float16形式。它可以將模型大小壓縮1倍,與int8相比能夠獲得更小的精度損失,它的前提需要你使用的硬件支持FP16操作,而FP16操作僅在一些設備上面才能使用。使用該方法的示例代碼如下所示:

import tensorflow as tf
# 裝載預訓練模型
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
# 設置優化器
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 判斷當前的設備是否支持FP16操作
converter.target_spec.supported_types = [tf.lite.constants.FLOAT16]
# 執行轉換
tflite_quant_model = converter.convert()

8.3 tensorflow訓練時量化詳解

  tensorflow中的訓練時量化是一種僞量化它是在可識別的某些操作內嵌入僞量化節點(fake quantization nodes),用以統計訓練時流經該節點數據的最大值和最小值,便於在使用TOCO轉換tflite格式時量化使用並減少精度損失,它參與模型訓練的前向推理過程令模型獲得量化損失,但梯度更新需要在浮點下進行因而其並不參與反向傳播過程。某些操作無法添加僞量化節點,這時候就需要人爲的去統計某些操作的最大最小值,但如果統計不準確那麼將會帶來較大的精度損失,因而需要謹慎檢查哪些操作無法添加僞量化節點。值得注意的是,僞量化節點的意義在於統計流經數據的最大最小值並參與前向傳播過程來提升模型精度,但其在TOCO工具轉換爲量化模型後,其工作原理還是與訓練後量化方式一致的!具體的量化流程如下圖所示:
在這裏插入圖片描述

  對於Relu節點而言,由於其支持量化操作,輸入的數據仍然是FP32類型的,使用Min和Max函數分別來統計輸入數據中的最大值和最小值;然後在Relu層的前面添加一個Quantize層來獲得量化後的QuantizedRelu,此時已經轉化爲INT8類型;接着執行相應的Relu計算(INT8類型);接着添加一個dequantize層來將INT8的結果轉換爲FP32,即該層的最終輸出仍然是FP32類型的。需要注意的是,當多個可識別的操作相鄰時,多個quantize和dequantize連接時是可以相互抵消的

8.3.1 訓練時量化代碼實戰

步驟1-在訓練圖結構內添加僞量化節點

# 獲取loss函數
loss = tf.losses.get_total_loss()
# 獲取原始的圖,並在原始的圖的基礎上創建一個量化圖
g = tf.get_default_graph()
tf.contrib.quantize.create_training_graph(input_graph=g, quant_delay=2000000)
# 設計優化器並執行反向傳播
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
optimizer.minimize(loss)

  一般是在loss之後optimizer之前添加tf.contrib.quantize.create_training_graph關鍵函數,其將自動的幫我們在可識別的操作上嵌入僞量化節點,訓練並保存模型後,模型圖結構就會自動的存在僞量化節點及其統計的參數。tf.contrib.quantize.create_training_graph的參數input_graph表示訓練的默認圖層,quant_delay是指多少次迭代之後再進行量化,如果是已訓練好進行微調量化的話,那麼可以將quant_del ay設爲0。

步驟2-重寫推理圖結構並保存爲新的模型

# 設置loss函數
logits = tf.nn.softmax_cross_entropy_with_logits_v2(...)
# 獲取原始的圖,並在原始的圖的基礎上創建一個量化圖
g = tf.get_default_graph()
tf.contrib.quantize.create_eval_graph(input_graph=g)
# 保存量化後的圖文件
with open(eval_graph_file, ‘w’) as f:
  f.write(str(g.as_graph_def()))
saver = tf.train.Saver()
saver.save(sess, checkpoint_name)

由於推理和訓練的僞量化圖結構存在着較大差異,該操作的作用是消除量化操作對BN層的影響。

步驟3-轉換模型爲全量化模型
首先,需要對第二步重寫後的模型執行固化操作。

freeze_graph \
  --input_graph=eval_graph_def.pb \
  --input_checkpoint=checkpoint \
  --output_graph=frozen_eval_graph.pb --output_node_names=outputs

然後,使用TOCO轉換得到真正的量化模型。

toco \
  --input_file=frozen_eval_graph.pb \
  --output_file=tflite_model.tflite \
  --input_format=TENSORFLOW_GRAPHDEF --output_format=TFLITE \
  --inference_type=QUANTIZED_UINT8 \
  --input_shape="1,224, 224,3" \
  --input_array=input \
  --output_array=outputs \
  --std_value=127.5 --mean_value=127.5

8.4 tensorflow量化方法比較

兩種量化的相同點如下所示:

  • 兩者均可達到模型量化的作用;
  • 兩者的推理工作原理是一樣的;
  • 兩者都可工作在Tensorflow lite推理框架下並進行相應加速;

兩種量化的不同點如下所示:

  • 前者是一種offline的方式,而後者則是一種online的方式;
  • 訓練後量化工作量稍微簡單些,而量化感知訓練工作量更繁瑣一些;
  • 量化感知訓練比訓練後量化損失的精度更少,官方推薦使用量化感知訓練方式;

8.5 Tensorflow量化效果展示與分析

在這裏插入圖片描述
  上圖展示了Tensorflow兩種不同的量化方式在不同模型上面的量化效果。該表中所有單幀推理時間都是在使用單個大內核的 Pixel 2 設備上測量的,通過該表我們可以得出初步的結論:訓練時量化比訓練後量化效果更好

9、MxNet模型量化詳解

9.1 簡介

  具體的細節請參考該鏈接
  MxNet當前支持的模型量化方式包括兩種,即使用英特爾®MKL-DNN進行模型量化和使用CUDNN進行模型量化

9.2 使用英特爾MKL-DNN進行模型量化

  英特爾®MKL-DNN支持通過英特爾®CPU平臺上的子圖功能進行量化,並可以在英特爾®至強®可擴展平臺上帶來性能改進。設計了一個新的量化腳本imagenet_gen_qsym_mkldnn.py,以使用英特爾®MKL-DNN啓動圖像分類模型的量化。該腳本與Gluon-CV modelzoo集成在一起,因此可以從Gluon-CV下載更多經過預訓練的模型,然後進行轉換以進行量化。要將量化流程直接應用於您的項目,請參閱使用MKL-DNN後端量化自定義模型

9.2.1 環境配置

pip install gluoncv
pip install mxnet-mkldnn

9.2.2 工具實戰

  從Gluon-CV下載預訓練的模型,並將其轉換爲最終將被量化的符號模型。該標定數據集可用於測試預先訓練的模型。

python imagenet_gen_qsym_mkldnn.py --model=resnet50_v1 --num-calib-batches=5 --calib-mode=naive

  執行上述操作之後,模型將會自動執行融合和量化操作,並將量化之後的結果保存在./model文件夾中。我們可以通過執行下面的代碼來進行量化後的模型推理。

# Launch FP32 Inference
python imagenet_inference.py --symbol-file=./model/resnet50_v1-symbol.json --param-file=./model/resnet50_v1-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu

# Launch INT8 Inference
python imagenet_inference.py --symbol-file=./model/resnet50_v1-quantized-5batches-naive-symbol.json --param-file=./model/resnet50_v1-quantized-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu

# Launch dummy data Inference
python imagenet_inference.py --symbol-file=./model/resnet50_v1-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
python imagenet_inference.py --symbol-file=./model/resnet50_v1-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True

9.3 使用CUDNN進行模型量化

  請在該鏈接下載該文件。此文件夾包含使用或不使用校準對FP32模型進行量化以及使用校準的量化進行推理的示例。以兩個預先訓練的imagenet模型爲例進行量化。一個是Resnet-152,另一個是Inception with BatchNorm。校準數據集是用於測試預先訓練的模型的驗證數據集。

  • imagenet_gen_qsym.py此腳本提供了一個示例,使用FP32模型和校準數據集生成校準的量化模型。第一次啓動時,腳本會將用戶指定的模型(Resnet-152或Inception)和校準數據集分別下載到模型和數據文件夾中。生成的量化模型可以在模型文件夾中找到;
  • imagenet_inference.py該腳本用於計算驗證數據集上的FP32模型或量化模型的準確性,該數據已在中下載用於校準imagenet_gen_qsym.py;
  • launch_quantize.sh這是一個Shell腳本,可爲Resnet-152和帶有BatchNorm的Inception生成具有各種配置的各種量化模型。用戶可以將命令從腳本複製並粘貼到控制檯,以針對特定配置運行模型量化;
  • launch_inference.sh這是一個Shell腳本,用於計算通過調用生成的所有量化模型的精度launch_quantize.sh

10、總結

  所謂的模型量化就是將浮點存儲(運算)轉換爲整型存儲(運算)的一種模型壓縮技術。偶遇該技術可以可以極大的縮小模型的大小,提高模型的運行速度,從而滿足機器人、手機等嵌入式終端的需求,因而得到了工業界的大量應用。當前比較成熟的模型量化技術主要分爲兩種,即訓練後量化和訓練時量化,訓練時量化不僅能夠達到量化效果,同時還可以獲得準確的量化結果。當前的很多深度學習框架中已經將模型量化方法嵌入其中,便於用戶的模型部署。除此之外,無論是移動端還是服務器端,都可以看到新的計算設備正不斷迎合量化技術,因而量化技術一定會越來越完善,從而進一步推動深度學習模型在低功率、低成本、低性能的終端上面的部署

參考資料

[1] 線性量化
[2] 神經網絡量化簡介
[3] pytorch模型量化
[4] tensorflow模型量化
[5] mxnet模型量化

注意事項

[1] 該博客是本人原創博客,如果您對該博客感興趣,想要轉載該博客,請與我聯繫(qq郵箱:[email protected]),我會在第一時間回覆大家,謝謝大家的關注。
[2] 由於個人能力有限,該博客可能存在很多的問題,希望大家能夠提出改進意見。
[3] 如果您在閱讀本博客時遇到不理解的地方,希望您可以聯繫我,我會及時的回覆您,和您交流想法和意見,謝謝。
[4] 本人業餘時間承接各種本科畢設設計和各種小項目,包括圖像處理(數據挖掘、機器學習、深度學習等)、matlab仿真、python算法及仿真等,有需要的請加QQ:1575262785詳聊,備註“項目”!!!

發佈了55 篇原創文章 · 獲贊 469 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章