目录:
我发现点下面的链接会跳到一个不知道是谁的CSDN下面需要付费下载,这个很迷惑,麻烦自行复制下面的链接。
Github:https://github.com/MasLiang/CNN-On-FPGA
那个不知道是谁的链接:https://download.csdn.net/download/weixin_42138780/18551586
没有下载不让举报,有办法的朋友麻烦举报一下
这一节主要是软件层面的处理。之所以要进行数据的量化,一个是为了能够使用低bit来表达整个网络从而达到对其压缩的目的,减小对存储空间的需求;二就是FPGA中的DSP单元属于定点数DPS单元,更善于处理定点数运算。
定点数量化方面的研究有很多,基本上16bit不会对模型造成损失,使用8bit经过一些trick之后损失也很小,研究方面人们已经开始关注更低的位宽,但是目前在实现上,完全使用更低位宽确实会带来较大的损失,因此最佳的办法是使用动态的量化方式,也就是说对于不同的层不同的数据使用不同的量化位宽。动态量化方面也有很多研究,比如之前达摩院提出过一个使用很复杂的数学方法算出来每一层的最佳量化位宽(反正我是没看懂)。这种研究层面的东西不是我们的重点,我们的重点在于实现,仍然是demo的出发点:一切以简单为主,主要是来说明整个流程,所以我们先不去考虑每一层的最优量化位宽是多少,统一用一个较大的位宽(16bit)来处理,至于用更低的位宽和动态的量化位宽,那其实是软件层面的事情,去复现那些现有的研究就可以。当然如果有研究兴趣的同学,目前低bit量化是一个害挺热门的方向。
pytorch1.4开始提供了8bit量化工具,有兴趣的同学可以探索一下,不过据说并不是很好用。
对数据进行量化最简单的方式就是将已经训练好的参数直接进行均匀量化和解量化。举个例子:
q
(
x
)
=
f
l
o
o
r
(
x
/
s
c
a
l
e
+
z
e
r
o
)
q(x) = floor(x/scale+zero)
q(x)=floor(x/scale+zero)
这是最常见的量化方式(这里使用了截位,为什么要用截位?为什么不用四舍五入?显然是四舍五入麻烦呀~~,当然有的文章中为了性能不仅仅不用四舍五入,甚至用随机取舍等,这个有兴趣的自己研究一下啦,毕竟这些人是做CV的不是做FPGA的),也就是说先确定量化的上下界,也就能确定你所要表达的数据所在的位置,然后
f
l
o
o
r
(
x
m
a
x
/
s
c
a
l
e
+
z
e
r
o
)
=
q
m
a
x
floor(x_{max}/scale+zero)=q_{max}
floor(xmax/scale+zero)=qmax
f
l
o
o
r
(
x
m
i
n
/
s
c
a
l
e
+
z
e
r
o
)
=
q
m
i
n
floor(x_{min}/scale+zero)=q_{min}
floor(xmin/scale+zero)=qmin
z
e
r
o
zero
zero代表着一个偏置,因为如果直接将
x
x
x进行放缩可能不能够恰好的占满整个量化区间的上下界,也就是不一定能够恰好满足上面两个式子。
使用这样的方式对输入数据
x
x
x、权重
w
w
w和偏置
b
b
b进行量化之后,我们得到了
x
q
x_q
xq、
w
q
w_q
wq和
b
q
b_q
bq,所以我们的计算公式变成了:
f
q
(
x
)
=
w
q
⊗
x
q
+
b
q
f_q(x)=w_q\otimes x_q+b_q
fq(x)=wq⊗xq+bq
因此对于卷积层的输出我们还需要进行解量化,这就是一个很麻烦的事情,因为我们对于
x
x
x、
w
w
w、
b
b
b使用的
z
e
r
o
zero
zero,
s
c
a
l
e
scale
scale都不同,这方面属于软件层面的trick,也有许多人去研究,我们暂时不做考虑如何能够更小误差的解量化,我们首先做一个trick来使得解量化变得简单。
影响我们解量化误差大小的,主要在于对不同的数据存在不同的
z
e
r
o
zero
zero,那么如果我们去掉这一项,情况立马发生变化
q
(
x
)
=
f
l
o
o
r
(
x
/
s
c
a
l
e
)
q(x) = floor(x/scale)
q(x)=floor(x/scale)
那么我们解量化的时候只需要
f
(
x
)
=
f
q
(
x
)
×
s
c
a
l
e
x
×
s
c
a
l
e
w
f(x)=f_q(x)\times scale_x\times scale_w
f(x)=fq(x)×scalex×scalew
要注意的是,我们需要保证
s
c
a
l
e
x
×
s
c
a
l
e
w
=
s
c
a
l
e
b
scale_x\times scale_w = scale_b
scalex×scalew=scaleb
而此时引入的量化误差,仅仅来自于在对数据进行量化的时候使用取整函数带来的误差,相对来说要小了很多。
当然这样做的问提也是很显著的,我们的量化区间并没有被完整的利用。我们这样做的
s
c
a
l
e
scale
scale将由以下公式决定:
f
l
o
o
r
(
x
m
a
x
/
s
c
a
l
e
)
≤
q
m
a
x
floor(x_{max}/scale)\leq q_{max}
floor(xmax/scale)≤qmax
f
l
o
o
r
(
x
m
i
n
/
s
c
a
l
e
)
≥
q
m
i
n
floor(x_{min}/scale)\ge q_{min}
floor(xmin/scale)≥qmin
也就是说在绝大多数情况下我们只能满足上下界中的一个。而不能完整利用量化区间带来的问提就是为了达到同样的精度需要增大量化区间,也就需要增大量化位宽,存储需求就增大了,凉凉。所以总是要有牺牲的,那么现在的很多研究就是在减小这个牺牲。
前面说的这种方式是直接对训练好的模型这样进行处理,又由于我们demo很小,板子空间又蛮大的,所以我们使用的是16bit量化位宽,显然我们有大把的空间可以去浪费(抱歉,有钱就是可以为所欲为.gif)。但是我们肯定是希望使用更小的位宽了,所以在后面我做其他项目的时候,我还是增加了
z
e
r
o
zero
zero进去的,解量化用的和没有
z
e
r
o
zero
zero同样的方式,为了减小这个误差,我又将量化后的模型在pytorch上面retrain了一下,误差减小了很多,在工程可接受范围内。当然这上面各种各样的方法有很多,因为我并不是做这方面研究的,所以只是以实现工程要求为目标,并没有过多的去考虑该如何做。
现在来考虑结合一下FPGA,FPGA最擅长的是什么呢,逻辑运算,举个例子,左移一位和乘以2,虽然得到的效果一样,但是FPGA更擅长前者。因此我们针对于在FPGA实现做进一步优化,我们需要保证
s
c
a
l
e
scale
scale是2的幂,这样一来不管是
×
s
c
a
l
e
\times scale
×scale还是
/
s
c
a
l
e
/scale
/scale都可以使用移位来进行。
那么在FPGA如何进行量化与解量化呢?
首先权重和偏置肯定是我们量化好存进去的,输入图像也是量化好的,重点在于中间过程的量化与解量化。我们还是先回到这个简单的demo上面,不使用
z
e
r
o
zero
zero,而且保证
s
c
a
l
e
scale
scale是2的幂 ,实现起来非常简单,就只需要简单的移位截位进位就好。
移位不用多说,首先是截位,比如说我们的两个8bit定点数乘法的输出是一个16bit的定点数,而这时候如果我们只需要高8位,那么只需要直接截断就好。
这里注意的是进位,由于四舍五入,对于正数来说,四舍五入就是让截位后的数字+最低位后一位数字,因为对于定点数而言,我们截位的地方就是我们的小数点,小数点后面第一位如果是1,那么就以为这小数大于等于0.5,如果是0,就是小于0.5。对于负数来说,因此四舍五入的时候应该怎么处理呢?留个思考题2333
在实际的运行当中,层间的解量化与量化是连在一起的,因此可以将他们合并处理。假设第
l
l
l层的输出数据尺度因子为
s
c
a
l
e
l
scale_l
scalel,那么这一层的解量化为:
f
l
(
x
)
=
f
q
l
(
x
)
/
s
c
a
l
e
l
f_l(x)=f_{ql}(x)/scale_l
fl(x)=fql(x)/scalel
而对于第
l
+
1
l+1
l+1层的输入来说,我们需要再重新对他进行量化,这时候的尺度因此为
s
c
a
l
e
(
l
+
1
)
d
a
t
a
scale_{(l+1)_{data}}
scale(l+1)data,那么我们这时候的量化为:
x
q
=
f
l
(
x
)
∗
s
c
a
l
e
(
l
+
1
)
d
a
t
a
x_q = f_l(x)*scale_{{(l+1)}_{data}}
xq=fl(x)∗scale(l+1)data
也就是说
x
q
=
f
q
l
(
x
)
×
(
s
c
a
l
e
(
l
+
1
)
d
a
t
a
/
s
c
a
l
e
l
)
x_q = f_{ql}(x)\times(scale_{(l+1)_{data}}/scale_l)
xq=fql(x)×(scale(l+1)data/scalel)
而这些尺度因子都是我们在软件层面提前设计好的,在实现的时候,我们可以直接使用一次量化操作,也就是这时候的尺度因此就是
s
c
a
l
e
(
l
+
1
)
d
a
t
a
/
s
c
a
l
e
l
scale_{(l+1)_{data}}/scale_l
scale(l+1)data/scalel,而我们尺度因子都是2的幂,所以最后我们只需要确定新的移位方向和移动位数就好。
要注意的是,如果我们仅仅是实现前向传播,那么我们并不需要对最后的输出进行解量化,因为分类的时候我们比较的是概率输出,只要不影响相对大小关系,就不会影响最终的结果,与量化与否无关。
另外对于包含了BN层的网络来说,由于BN层的参数在训练结束后是固定的,因此可以将其融入到卷积层当中
f
(
x
)
=
w
⊗
x
+
b
f(x)=w\otimes x+b
f(x)=w⊗x+b
f
B
N
(
x
)
=
(
f
(
x
)
−
m
e
a
n
)
/
v
a
r
f_{BN}(x)=(f(x)-{mean})/\sqrt{var}
fBN(x)=(f(x)−mean)/var
融合之后得到:
f
(
x
)
=
(
w
/
v
a
r
)
⊗
x
+
(
b
−
m
e
a
n
)
/
v
a
r
f(x)=(w/\sqrt{var})\otimes x+(b-mean)/\sqrt{var}
f(x)=(w/var)⊗x+(b−mean)/var
这里面的
m
e
a
n
mean
mean和
v
a
r
var
var都是训练集得到的固定值,所以就是得到了新的
w
w
w和
b
b
b。
对于离线模型的量化暂时说这么多,还有很多更复杂的方法,这不是我们简单demo的范围内,下一节将会讲解如何在训练中加入自己的量化,虽然这个过程完全使用Pytorch实现(当然也会有人使用其他框架实现),暂时彻底脱离了FPGA,但是最终得到的模型可以很easy的在FPGA上进行部署。