随着深度学习的不断发展,神经网络广泛应用于不同的领域,取得远超以往的效果的同时深度网络模型的复杂度也越来越高, 这严重制约了它在工业界发展.
模型量化是目前工业界最有效的模型优化方法之一.
模型量化是指把模型的参数从FP32映射到n
bit位的过程, 简单来说就是在定点数与浮点数等数据之间建立一种数据映射关系, 使得以较小的精度损失代价获得了较好的收益。 例如FP32-->INT8可以实现4倍的参数压缩,在压缩内存的同时可以实现更快速的计算,从而有效地提高模型的性能; 最极端的二值量化理论上甚至可以实现32倍的压缩,但是过度的压缩会导致模型的精度快速下降, 所以更多量化的时候需要做好精度和性能的权衡.
工业界一般使用int8量化, 在模型推理前需要把FP32映射为int8进行计算, 然后在输出的时候做一个去量化操作, 把计算的int8结果映射回FP32.
在介绍量化原理之前, 我们至少需要回答3个问题:
计算量越大的模型, 性能一般越差, 我们知道CPU或者GPU的FLOPS(每秒浮点计算量)是相对固定的, 模型的计算量大小是决定了模型性能的上限.
通过上图, 我们可以看到不同的数据类型的表示范围和精度完全不同, 简单地做类型转换显然是不行的.
量化的本质就是对float32进行缩放, 即: Q=R/S+Z
R 表示真实的浮点值,Q 表示量化后的定点值,Z 表示 0 浮点值对应的量化定点值,S 则为缩放因子。
按照量化级的划分方式来分, 假设现在要把FP32映射到int8, 那么有:
根据Z(零点)的不同, 我们又分为对称量化和非对称量化
按照阈值选择的不同划分, 我们又分为饱和量化/非饱和量化
在模型训练好后量化, 量化其实就是权重和激活值进行缩放的过程,在PTQ中, 我们是通过统计的方法, 使用Calibration Dataset来近似模拟现实的数据分布, 从而得到
权重和激活值的动态范围(Gather layer statistics) 和量化参数(q-parms), 以此来对我们的权重和激活值进行合理的缩放。
在模型训练阶段进行量化, 由于PTQ可能存在一些误差,所以我们需要一种可学习的scale。
训练中量化QAT 就是在做这样一件事情。简单概括就是,我们在网络训练过程去模拟量化,我们通过设定一个可学习的scale,这个scale一般可以与weights或者激活值相绑定,然后我们利用一个量化过程 q = round(r/s)127,将需要量化的值量化到0, 127之间,再接着一个反量化过程q s,就实现了一个误差的传递,接着我们利用反量化后的结果继续前传,最后得到loss,我们求量化后权重的梯度,并用它来更新量化前的权重,使得这种误差被网络抹平,让网络越来越像量化后的权重靠近,最后我们得到了量化后的权重q和缩放因子s。而这一系列操作都可以写成网络中的一个op,实现网络的正常训练。最后,我们利用q和s,来进行线上推理。
QAT中我们可以直接对指定位置的指定操作进行量化或者去量化, 这完全取决于我们是希望获得精度还是速度.
如上图所示, 一个预训练好的模型, 它的权重一般分布比较聚集, 比较均匀, 所以这部分Nvidia直接使用不饱和量化&对称量化, 对称量化的Z等于0, 所以只需要求scale, 具体操作如下:
据说刚开始Nvidia还使用了偏置项(如左图), 后来发现这一项对模型精度没有影响就去掉了(如右图), 其实就是做简单的线性缩放, 其中 $scale_A =\frac{1}{|max_A|}, $ $scale_B =\frac{1}{|max_B|}$, scale_A, scale_B表示的是缩放因子.
真正麻烦的是激活值的量化,Nvidia统计了3种模型的不同layer的激活值分布情况如下:
这里的x轴表示的是激活值, y轴是激活值的出现的次数次数统计后进行标准化的结果, 每一个样本的样本参数分布都是一个散点图, 由于神经网络一般使用Relu做中间层的激活函数, 通过下面的函数图可以看到这个函数的激活值有下界但是没有上界, 所以很多时候激活值的范围都是比较大的.
事实也是如此, 通过上面的3个激活值分布图可以看出:
由于存在离散点噪声的原因, Nvidia在对激活值量化时, 不再是基于最大值进行缩放, 而是找一个阈值|T|, 以此来控制量化的float范围, 避免离散点噪声被放大,
通过前面的介绍我们也知道这种方法叫做饱和量化, 关键是如何找到最优的阈值?
TensorRT 为了找到一个最优的阈值, 引入了KL散度(也称相对熵)来衡量不同的INT8分布与原来的FP32分布之间的差异程度, 不了解这个熵理论的可以看这里信息熵理论, 它的公式如下:
KL_{divergence}(P,Q):= \sum_{i=1}^n (P[i]*log(P[i]/Q[i])
其中P,Q分别称为实际分布(reference_distribution)、量化分布(quantize_distribution), KL散度越小, 说明两个分布约接近, nvidia把确定每一层的|T|值的过程被称为校准(Calibration), 大体的流程如下:
TensorRT官方也给出了这一个过程的伪代码:
nvidia给出了一个demo实例:
import scipy.stats as stats
import numpy as np
test_p = [1, 0, 2, 3, 5, 3, 1, 7]
spilt_array = np.array_split(test_p, 2)
ret_q = []
for arr in spilt_array:
avg = np.sum(arr)/np.count_nonzero(arr)
for item in arr:
if item!=0:
ret_q.append(avg)
continue
ret_q.append(0)
print(ret_q)
test_p/=np.sum(test_p)
ret_q/=np.sum(ret_q)
print("KL(P,Q)=", stats.entropy(test_p, ret_q))
具体细节如下:
step1: 首先将激活值分成2048组, 每组包含多个数值, 注意如果激活值的范围大小没有超过128, 那么是可以直接进行映射的.
step2: 不断地截断参考样本P,长度从128开始到2048, 截断区外的值加到截断样本P的最后一个值之上;最后求得样本P的概率分布;
step3: 创建样本分布Q,其元素的值为截断样本P的int8量化值, 将Q样本长度拓展到 i ,使得和原样本P具有相同长度;求得Q的概率分布 并计算P、Q的KL散度值
step4: 不断循环step2, step3, 就能不断地构造P和Q并计算相对熵,然后找到最小(截断长度为m)的相对熵, 而阀值就等于(m + 0.5)*一个bin的长度;
校准后的效果:
更多关于KL散度的说明请参考附录.
可以参考一下neural-compressor里面的实现: https://github.com/intel/neural-compressor/blob/master/neural_compressor/utils/kl_divergence.py
KL分布是用来衡量2个数据分布的距离的, 当两个分布越相近, KL值越小, 分布一样的话, KL=0, 其公式为:
H(f|g) = D_{KL}(f, g) = \sum_{x\in X}f(x)*log_2\ \frac{f(x)}{g(x)}
假设f(x)为真实分布X,g(y)为非真实分布Y,p表示事件发生的概率, 则上述相对熵为:
H(X|Y) = D_{KL}(X||Y) = \sum_{i=1}^{N}\ p(x)\ log_2\ \frac{p(x)}{p(y)}
所以我们可以用KL散度来找最优的分布情况, 例如现在我们随机一组数据表示原始数据的分布情况
import numpy as np
SIZE = 10
x = [np.random.uniform(1, SIZE+1) for i in range(SIZE)]
px = x / np.sum(x)
def get_KL():
y = [np.random.uniform(1, SIZE+1) for i in range(SIZE)]
py = y / np.sum(y)
KL = 0
for i in range(SIZE):
KL+=px[i]*np.log(px[i]/py[i])
return KL, y
然后我们可以使用暴力法找到一个KL在可接受范围内的分布:
from itertools import count
import matplotlib.pyplot as plt
def get_KL_Q(kl_threshold=0.01):
for i in count():
KL, y = get_KL()
if KL < kl_threshold:
plt.plot(x)
plt.plot(y)
break
return
我们看一下 kl_threshold=0.01
时, y的分布情况:
get_KL_Q(0.01)
我们看一下 kl_threshold=0.05
时, y的分布情况:
get_KL_Q(0.05)
显然, KL越小, Y和X就拟合的越好.
假设现在有一个float32的激活值的分布如下:
import random
import numpy as np
import matplotlib.pyplot as plt
def generator_P(size):
walk = []
avg = random.uniform(3.000, 600.999)
std = random.uniform(500.000, 1024.959)
for _ in range(size):
walk.append(random.gauss(avg, std))
return walk
size = 20480
P = generator_P(size)
P = np.array(P)
P = P[P>0]
print("最大的激活值", max(np.absolute(P)))
# 可视化
plt.title("Relu activation value Histogram")
plt.xlabel("Activation values")
plt.ylabel("Normalized number of Counts")
# bins表示要分成多少组, density表示是否要normed
n, bins, _ = plt.hist(P, bins=2047, density=True)
plt.show()
print("各个组的范围", bins)
print("各个组包含的样本概率", n)
print("步长:", bins[2]-bins[1])
def smooth_distribution(p, eps=0.0001):
"""
给定离散分布(可能尚未标准化为1),通过将0替换为eps乘以比例因子使其平滑, 并从非零值中减去相应的量。
"""
is_zeros = (p == 0).astype(np.float32)
is_nonzeros = (p != 0).astype(np.float32)
n_zeros = is_zeros.sum()
n_nonzeros = p.size - n_zeros
if not n_nonzeros:
raise ValueError('The discrete probability distribution is malformed. All entries are 0.')
eps1 = eps * float(n_zeros) / float(n_nonzeros)
assert eps1 < 1.0, 'n_zeros=%d, n_nonzeros=%d, eps1=%f' % (n_zeros, n_nonzeros, eps1)
hist = p.astype(np.float32)
hist += eps * is_zeros + (-eps1) * is_nonzeros
assert (hist <= 0).sum() == 0
return hist
import matplotlib.pyplot as plt
import copy
test_p = np.array([0, 2, 3, 4, 2.4 ,5, 6])
plt.plot(test_p, color='r')
smooth_test_p = copy.deepcopy(test_p)
smooth_test_p = smooth_distribution(temp_p, eps = 0.5)
plt.plot(smooth_test_p, color='g')
plt.show()
import copy
import scipy.stats as stats
def threshold_distribution(distribution, target_bin=128):
distribution = distribution[1:]
length = distribution.size
threshold_sum = sum(distribution[target_bin:])
kl_divergence = np.zeros(length - target_bin)
for threshold in range(target_bin, length):
sliced_nd_hist = copy.deepcopy(distribution[:threshold])
# generate reference distribution p
p = sliced_nd_hist.copy()
p[threshold - 1] += threshold_sum
threshold_sum = threshold_sum - distribution[threshold]
# is_nonzeros[k] indicates whether hist[k] is nonzero
is_nonzeros = (p != 0).astype(np.int64)
quantized_bins = np.zeros(target_bin, dtype=np.int64)
# calculate how many bins should be merged to generate
# quantized distribution q
num_merged_bins = sliced_nd_hist.size // target_bin
# merge hist into num_quantized_bins bins
for j in range(target_bin):
start = j * num_merged_bins
stop = start + num_merged_bins
quantized_bins[j] = sliced_nd_hist[start:stop].sum()
quantized_bins[-1] += sliced_nd_hist[target_bin * num_merged_bins:].sum()
# expand quantized_bins into p.size bins
q = np.zeros(sliced_nd_hist.size, dtype=np.float64)
for j in range(target_bin):
start = j * num_merged_bins
if j == target_bin - 1:
stop = -1
else:
stop = start + num_merged_bins
norm = is_nonzeros[start:stop].sum()
if norm != 0:
q[start:stop] = float(quantized_bins[j]) / float(norm)
# 平滑处理, 保证KLD计算出来不会无限大
p = smooth_distribution(p)
q = smooth_distribution(q)
# calculate kl_divergence between q and p
kl_divergence[threshold - target_bin] = stats.entropy(p, q)
min_kl_divergence = np.argmin(kl_divergence)
threshold_value = min_kl_divergence + target_bin
return threshold_value
# 获取KL最小的阈值
hist, bins = np.histogram(P, bins =2048)
threshold = threshold_distribution(hist, target_bin=128)
print("threshold 所在组:", threshold)
print("threshold 所在组的区间范围:", bins[threshold])
# 分成split_zie组, density表示是否要normed
plt.title("Relu activation value Histogram")
plt.xlabel("Activation values")
plt.ylabel("Normalized number of Counts")
plt.hist(P, bins=2047)
plt.vlines(bins[threshold], 0, 30, colors = "r", linestyles = "dashed")
plt.show()
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。