推理加速主要针对串行加速
1. SVD分解
A m × n = V m × m T ( λ 1 1 2 λ 2 1 2 ⋱ ) U n × n A_{m \times n}=V_{m \times m}^{T}\left(\begin{array}{cccc} \lambda_{1}^{\frac{1}{2}} & & \\ & & \lambda_{2}^{\frac{1}{2}} & \\ & & & \ddots & \\ & & & \end{array}\right) U_{n \times n} Am×n=Vm×mT⎝⎜⎜⎜⎛λ121λ221⋱⎠⎟⎟⎟⎞Un×n
SVD分解进行加速主要分两步
- Pretrain. 先对一个大的神经网络进行训练,训练个几轮之后,得到一个初步的精度结果。
- SVD + fine tuning。然后对训练好的大神经网络进行SVD,然后对SVD后的神经网络进行训练,得到一个更好的精度结果。
比如说,原来的大神经网络是一个两层1000个神经元的网络,做了SVD之后,变成了1000,256,1000的三层神经网络,参数量由原来的 1000 × 1000 1000\times 1000 1000×1000变为了 1000 × 256 + 256 × 1000 1000\times 256 + 256\times 1000 1000×256+256×1000,参数量就从1000000降到了差不多一半。
有人会问,为何不直接训练1000,256,1000的三层神经网络呢?因为如果直接训练这个参数少的矩阵,很有可能会得到一个性能下降很厉害的结果。而先训练两层网络得到一个大的参数矩阵 W W W之后,再做SVD得到两个参数矩阵 W 1 , W 2 W_1, W_2 W1,W2,这两个矩阵其实尽量保留了 W W W的信息。再做fine tuning就能更快的得到更好的结果。
也就是说
- 直接训练三层网络,得到的最优结果可能准确率也就85%
- 先训练两层网络,先得到一个95%准确率的优化结果,但是参数太多;做SVD,直接预测,得到一个90%准确率的结果。这依然比直接训练3层网络得到的准确率高。再训练几轮微调,可能又得到95%的准确率结果。
因为第1种方案的初始参数是随机生成的参数,没有什么先验信息。而第2中方案的三层网络是由训练好的两层网络得到的,已经把相当一部分信息参数带过来了。
2. Hidden Node prune
还是假设我们有一个1000*1000的两层神经网络,经过训练之后,有了一个参数矩阵
我们对第二层每个神经元与前一层神经元链接的参数做一个求和,比如我们这里有 ∑ i ∣ w i 1 ∣ \sum_{i}\left| w_{i1} \right| ∑i∣wi1∣,和 ∑ i ∣ w i 2 ∣ \sum_{i}\left| w_{i2} \right| ∑i∣wi2∣, 如果第一个神经元连接的参数和比较大,说明这个神经元比较重要,我们要予以保留。但是如果第二个神经元连接的参数和比较小,比如只有0.1,那么说明这个神经元起的作用不大,我们可裁剪权重和的值比较小的这个隐藏层结点。
具体到所有结点,我们可以把每个结点的权重和做一个排序,比如只保留前面最大的60%的结点,剩下的40%的结点都裁剪掉。这样显然会有精度下降,因此我们还是要再做一次fine tuning.
- pretrain
- Hidden node prune + fine tuning.
我们既可以考虑前面到某个神经元结点的权重,也可以考虑从某个神经元结点到后面的权重。
3. 知识蒸馏
就是从一个已经训练好的大模型“教”一个小模型。
3.1 KL距离
KL距离,是kullback-leibler差异的简称,也称为相对熵。它衡量的是相同事件空间里的两个概率分布的差异情况。物理意义是:在相同事件空间里,概率分布
P
(
x
)
P(x)
P(x)对应的每个事件,若用概率分布
Q
(
x
)
Q(x)
Q(x)编码时,平均每个基本事件(符号)编码长度增加了多少比特。
D
(
P
∥
Q
)
=
∑
N
∈
X
P
(
x
)
log
P
(
x
)
Q
(
x
)
D(P \| Q)=\sum_{N \in X} P(x) \log \frac{P(x)}{Q(x)}
D(P∥Q)=N∈X∑P(x)logQ(x)P(x)
当两个概率分布完全相同时,也就是
P
(
X
)
=
Q
(
X
)
P(X)=Q(X)
P(X)=Q(X),其相对熵是0.
我们知道,概率分布
P
(
X
)
P(X)
P(X)的信息熵为
H
(
p
)
=
∑
p
(
x
)
log
1
p
(
x
)
H(p)=\sum p(x) \log \frac{1}{p(x)}
H(p)=∑p(x)logp(x)1
如果用一个新的非真实分布
Q
(
X
)
Q(X)
Q(X)来表示来自真实分布
P
(
X
)
P(X)
P(X)的平均编码长度,就是
H
(
p
,
q
)
=
∑
p
(
x
)
log
1
q
(
x
)
H(p, q)=\sum p(x) \log \frac{1}{q(x)}
H(p,q)=∑p(x)logq(x)1
这时
H
(
p
,
q
)
H(p, q)
H(p,q)就是交叉熵。比如我们有一个含有4个字母(A, B, C, D)标记的数据集中,真实标签分布是
p
=
(
1
/
2
,
1
/
2
,
0
,
0
)
p=(1/2, 1/2, 0, 0)
p=(1/2,1/2,0,0),也即是A和B出现的概率均为1/2, C和D出现的概率为0.
H
(
p
)
=
log
2
=
1
H(p)=\log 2=1
H(p)=log2=1,也就是只需1位编码就能识别A和B。如果使用分布
q
=
(
1
/
4
,
1
/
4
,
1
/
4
,
1
/
4
)
q=(1/4, 1/4, 1/4, 1/4)
q=(1/4,1/4,1/4,1/4)来编码,
H
(
p
,
q
)
=
log
4
=
2
H(p, q)=\log 4=2
H(p,q)=log4=2,也就是需要2位编码来识别A和B。(这里C和D在真实分布中不会出现)
可见
D
(
P
∥
Q
)
=
H
(
P
,
Q
)
−
H
(
P
)
=
∑
x
P
(
x
)
log
P
(
x
)
Q
(
x
)
D(P \| Q)=H(P, Q)-H(P)=\sum_{x} P(x) \log \frac{P(x)}{Q(x)}
D(P∥Q)=H(P,Q)−H(P)=x∑P(x)logQ(x)P(x)
因此通常“相对熵”也可称为“交叉熵”,虽然公式上看相对熵=交叉熵-信息熵,但由于真实分布
P
P
P是固定的,
D
(
P
∥
Q
)
D(P\|Q)
D(P∥Q)由
H
(
P
,
Q
)
H(P,Q)
H(P,Q)决定。
【注意这里是针对一个样本,针对输出层每个分类结点的加和】
3.2 软标签vs硬标签
知识蒸馏的思想就是用大模型得到软标签,然后用作小模型的标注。
硬标签是通常我们人工标注出来的分类标签,比如下图中的绿色标签,5个分类只有一个分类输出结点是1,其他是0。但是这种分类标签其实留下的“余地”较小。但是如果一个新的样本有点像第4个结点,也有点像第5个结点。比如说一张图片有点像狗又有点像猫,那么用硬标签来训练可能效果就不太好。另外也要考虑到人工标注的错标记问题。因此我们引入软标签,也就是不要让分类标签那么绝对,分配一定的概率在不同标记上,如第二个绿色标签所示。
那么我们怎么得到软标签呢?就是先利用一个大的模型训练得到一批相对比较精确的预测结果,用这些预测结果作为软标签,就是图中的蓝色标签。然后用训练出来的软标签作为真实值去训练下方的小模型。假设这里小模型采用了softmax,那么根据小模型的预测和软标签的不同分布自然得到了相对熵损失函数
D
(
y
∥
S
)
=
∑
y
log
y
−
∑
y
log
S
D(y\|S)=\sum y\log y - \sum y\log S
D(y∥S)=∑ylogy−∑ylogS
又因为只有第二项与小模型训练有关,因此损失函数简化为
L
=
−
∑
i
=
0
n
−
1
y
i
log
S
i
=
−
∑
i
=
0
n
−
1
y
i
log
e
z
i
∑
j
=
0
n
−
1
e
z
j
=
−
∑
i
=
0
n
−
1
y
i
(
log
e
z
i
−
log
∑
j
=
0
n
−
1
e
z
j
)
=
−
∑
i
=
0
n
−
1
y
i
z
i
+
∑
i
=
0
n
−
1
y
i
log
∑
j
=
0
n
−
1
e
z
j
\begin{aligned} L&=-\sum_{i=0}^{n-1} y_{i} \log S_{i}\\ &=-\sum_{i=0}^{n-1} y_{i} \log \frac{e^{z_{i}}}{\sum_{j=0}^{n-1} e^{z_{j}}}\\ &=-\sum_{i=0}^{n-1} y_{i}(\log e^{z_{i}}-\log \sum_{j=0}^{n-1} e^{z_{j}})\\ &=-\sum_{i=0}^{n-1} y_{i} z_{i}+\sum_{i=0}^{n-1} y_{i} \log \sum_{j=0}^{n-1} e^{z_{j}} \end{aligned}
L=−i=0∑n−1yilogSi=−i=0∑n−1yilog∑j=0n−1ezjezi=−i=0∑n−1yi(logezi−logj=0∑n−1ezj)=−i=0∑n−1yizi+i=0∑n−1yilogj=0∑n−1ezj
所以在做反向传播的时候,对某一个输出结点
z
k
z_{k}
zk做求导:
∂
L
∂
z
k
=
−
y
k
+
∑
i
=
0
n
−
1
y
i
e
z
k
∑
j
=
0
n
−
1
e
z
j
=
−
y
k
+
∑
i
=
0
n
−
1
y
i
S
k
=
−
y
k
+
S
k
(
∑
i
=
0
n
−
1
y
i
)
=
S
k
−
y
k
\begin{aligned} \frac{\partial L}{\partial z_{k}} &=-y_{k}+\sum_{i=0}^{n-1} y_{i} \frac{e^{z_{k}}}{\sum_{j=0}^{n-1} e^{z_{j}}} \\ &=-y_{k}+\sum_{i=0}^{n-1} y_{i} S_{k} \\ &=-y_{k}+S_{k}\left(\sum_{i=0}^{n-1} y_{i}\right) \\ &=S_{k}-y_{k} \end{aligned}
∂zk∂L=−yk+i=0∑n−1yi∑j=0n−1ezjezk=−yk+i=0∑n−1yiSk=−yk+Sk(i=0∑n−1yi)=Sk−yk
所以写成向量形式就是
∂
L
∂
z
=
S
−
y
\dfrac{\partial L}{\partial z}=S-y
∂z∂L=S−y。
- pretrain大模型,得到精度95%
- 直接训练小模型,得到的精度可能只有80%
- 用大模型TS训练小模型,得到的精读可能是94%
4. LSTM为例子的内部参数共享
这个用的很少,只有少数paper提到这样的方法。也就是把LSTM中输入与四个门之间的四个参数矩阵,彼此之间用一个比例系数相互关联,同时一起训练
W
c
x
=
α
1
W
i
x
=
α
2
W
f
x
=
α
3
W
o
x
W_{cx}=\alpha_1 W_{ix} = \alpha_2 W_{fx} = \alpha_3 W_{ox}
Wcx=α1Wix=α2Wfx=α3Wox
这里的三个系数也是要训练出来的。
5. 神经网络的量化
在用L1,L2正则化进行训练时,参数w很容易接近于零。我们可以限制w的取值范围是[-10, 10]
这时w是浮点数,我们可以
w
1
′
=
[
w
1
×
2
32
]
2
32
w_{1}^{\prime} = \frac{\left[w_{1} \times 2^{32}\right]}{2^{32}}
w1′=232[w1×232]
这里的分子进行了取整操作,就意味着,原来的
w
1
×
2
32
2
32
\dfrac{w_{1} \times 2^{32}}{2^{32}}
232w1×232与
[
w
1
×
2
32
]
2
32
\dfrac{\left[w_{1} \times 2^{32}\right]}{2^{32}}
232[w1×232]顶多相差
1
2
32
\dfrac{1}{2^{32}}
2321.
假设我们神经网络中有两个参数w1, w2,都在[0,1]之间,都进行了这样的处理得到 w 1 ′ , w 2 ′ w_{1}^{\prime}, w_{2}^{\prime} w1′,w2′, w 1 ′ w_{1}^{\prime} w1′与 w 1 w_{1} w1相差最多只有 1 2 32 \dfrac{1}{2^{32}} 2321.
如果这两个参数要相乘, w 1 ′ × w 2 ′ w_{1}^{\prime}\times w_{2}^{\prime} w1′×w2′那么分母就是 2 64 2^{64} 264, 只要进行移位操作即可,分子是两个整数相乘,计算也相对容易。
这就是神经网络量化的核心思想:把浮点运算转化为正数运算,比如32bit, 16bit, 8bit。现在用的比较多的是8比特。还使用上面的两个参数作为例子
w
1
′
=
[
w
1
×
2
8
]
2
8
w
2
′
=
[
w
1
×
2
8
]
2
8
w_{1}^{\prime} = \frac{\left[w_{1} \times 2^{8}\right]}{2^{8}}\\ w_{2}^{\prime} = \frac{\left[w_{1} \times 2^{8}\right]}{2^{8}}
w1′=28[w1×28]w2′=28[w1×28]
那这样,分母上都是一个移位的操作,分子上都是8位的整数相乘。
运用intel的sse扩张指令集或者arm的neon进行加速运算(也就是硬件直接运算)
这样一次能同时运算多条乘法。一个寄存器有128位,如果我们都是8位的整数运算,就意味着同时能进行16个8位的乘法计算。一个寄存器就能加载16个权重参数。当然也可以转为8个16位的运算。
训练的时候前向后向传播算法也可以利用这种技巧进行微调。比如说某一个参数 w i w_i wi,通过8位量化,就映射到了集合 { i 2 8 } \{\dfrac{i}{2^{8}}\} {28i}中。因为小数8位量化取整后,分辨率就变成了 1 2 8 \dfrac{1}{2^{8}} 281,比如 w i w_{i} wi这个小数本来处于 [ 1 2 8 , 2 2 8 ] [\dfrac{1}{2^{8}}, \dfrac{2}{2^{8}}] [281,282]之间,它离两端谁更近,它就转化为这个分数。就是把隐层结点参数都映射到集合 { i 2 8 } \{\dfrac{i}{2^{8}}\} {28i}中,权重更新后,又把更新后的小数映射到集合中的某一个分数。
6. binary net二值化网络
参数就只有-1,1两个参数,放入8位单元中
采用位运算进行加速。
训练时候前向后向传播算法的微调
前向的时候
y
=
{
1
x
>
0
−
1
x
<
0
y=\left\{\begin{array}{ll} 1 & x>0 \\ -1 & x<0 \end{array}\right.
y={1−1x>0x<0
后向传播
∂
J
∂
x
=
∂
J
∂
y
∂
y
∂
x
\dfrac{\partial J}{\partial x}=\dfrac{\partial J}{\partial y}\dfrac{\partial y}{\partial x}
∂x∂J=∂y∂J∂x∂y
y
=
{
0
x
>
1
1
−
1
<
x
<
1
0
x
<
−
1
y=\left\{\begin{array}{ll} 0 & x>1 \\ 1 & -1<x<1\\ 0 & x<-1 \end{array}\right.
y=⎩⎨⎧010x>1−1<x<1x<−1
但是性能下降太快,用得不是很多
7. 基于FFT的循环矩阵
假设我们先初始化一个结点的参数
(
a
1
,
a
2
,
a
3
⋯
⋯
a
n
)
=
C
\left(\begin{array}{lll} a_{1}, & a_{2}, & a_{3} \cdots \cdots & a_{n} \end{array}\right)=\mathrm{C}
(a1,a2,a3⋯⋯an)=C
然后我们把这一层的结点的参数矩阵写为
(
a
1
,
a
2
,
a
3
⋯
⋯
a
n
a
n
,
a
1
,
a
2
⋯
⋯
a
n
−
1
a
n
−
1
,
a
n
,
a
1
⋯
⋯
a
n
−
2
a
2
⋯
a
1
)
=
w
\left(\begin{array}{cccc} a_{1}, & a_{2}, & a_{3} \cdots \cdots & a_{n} \\ a_{n,} & a_{1}, & a_{2} \cdots \cdots & a_{n-1} \\ a_{n-1}, & a_{n}, & a_{1} \cdots \cdots & a_{n-2} \\ a_{2} & \cdots & & a_{1} \end{array}\right)=w
⎝⎜⎜⎛a1,an,an−1,a2a2,a1,an,⋯a3⋯⋯a2⋯⋯a1⋯⋯anan−1an−2a1⎠⎟⎟⎞=w
这样
x
=
(
x
1
x
2
⋮
x
n
)
x=\left(\begin{array}{c} x_{1} \\ x_{2} \\ \vdots \\ x_{n} \end{array}\right)
x=⎝⎜⎜⎜⎛x1x2⋮xn⎠⎟⎟⎟⎞
w x = ( a 1 x 1 + ⋯ a n x n a n x 1 + ⋯ u n − 1 x n ⋮ ⋮ ) = c ∗ x w x=\left(\begin{array}{c} a_{1} x_{1}+\cdots a_{n} x_{n} \\ a_{n} x_{1}+\cdots u_{n-1} x_{n} \\ \vdots \\ \vdots \end{array}\right)=c*x wx=⎝⎜⎜⎜⎜⎛a1x1+⋯anxnanx1+⋯un−1xn⋮⋮⎠⎟⎟⎟⎟⎞=c∗x
这就是c和x之间的卷积操作。根据傅里叶变换有
F
F
T
(
c
∗
x
)
=
F
F
T
(
c
)
⋅
F
F
T
(
x
)
c
∗
x
=
F
F
T
−
1
[
F
F
T
(
c
)
⋅
F
F
T
(
x
)
]
FFT(c * x)=FFT(c) \cdot FFT(x)\\ c*x=FFT^{-1}\left[FFT(c) \cdot FFT(x)\right]
FFT(c∗x)=FFT(c)⋅FFT(x)c∗x=FFT−1[FFT(c)⋅FFT(x)]
也就是说为了得到
w
x
=
c
∗
x
wx=c*x
wx=c∗x,可以先算c和x的傅里叶变换,然后相乘,然后再做乘积的傅里叶逆变换。这个傅里叶变换计算速度非常快。