上文介绍了LPCNet的算法原理和工程,本文主要介绍LPCNet的加速方案之稀疏化处理。
我们首先了解GRU,然后再看作者如何对GRU进行稀疏化,来提升网络性能。
GRU的整个流程如下图所示:
重置门和更新门:重置门和更新门的输入为当前时刻输入X_t 和上一个时刻隐藏状态H_{t-1} ,通过全连接层和激活层得到输出
R_t和Z_t ,sigmoid激活所以值域0,1。
候选隐藏状态:当前时刻R_t 和上一时刻隐藏状态做乘法,结果和当前时刻输入结合,通过全连接和激活得到当前时刻候选隐藏状态,激活为tanh,s所以值域[-1,1]。
隐藏状态:最后,时刻t的隐藏状态是由当前时刻的Z_t 结合上一时刻的隐藏状态和当前时刻的候选隐藏状态组合得到。
以上就是GRU的流程,其中W为权重参数,B为偏差参数,具体参数量我们在下一节详细介绍。
keras实现GRU源码:
https://github.com/keras-team/keras/blob/v2.10.0/keras/layers/rnn/gru.py#L394-L905
注意其中DNNGRU和GRU实现的区别:
为了使用CuDNNGRU训练,兼容GRU,必须设置reset_after=Truerecurrent_activation="sigmoid"
GRU( recurrent_activation="sigmoid", reset_after='true')
另外bias的shape也有点区别
了解了GRU的过程,下面通过LPCNet里的使用来看GRUA的参数。
通过以上介绍,GRU参数如下:
W_{xr},W_{xz},W_{xh} X*W的权重:d(输入维度)*units(输出维度)*n_gates(3个门)
W_{hr},W_{hz},W_{hh} H*W的权重:units*units*3(3个门)
bias: 2*units*n_gates
在LPCNet的GRUA:
rnn = GRU(rnn_units1, return_sequences=True, return_state=True, recurrent_activation="sigmoid", reset_after='true', name='gru_a', stateful=True, recurrent_constraint = constraint, recurrent_regularizer=quant)
其中rnn_units1=384,GRUA输入N,L,512
三个循环无关的W_x参数量:512*_384*_3
三个循环相关的W_h参数量:384*_384*_3
另外还有bias:384*3*2
参数量:512*384*3+384*384*3+384*3*2=1034496
下面我们介绍LPCNet中如何实现稀疏化
代码如下:
#Training from scratch
sparsify = lpcnet.Sparsify(2000, 40000, 400, density)
grub_sparsify = lpcnet.SparsifyGRUB(2000, 40000, 400,args.grua_size, grub_density)
表示2000之前batch迭代不进行稀疏化;2000-40000每间隔400个迭代进行一次稀疏化;40000后每个迭代进行稀疏化,这里通过加一个callback对象sparsify。
针对gru_a层的三个和循环相关重置门,更新门,隐藏状态参数的W_{hr},W_{hz},W_{hh}进行稀疏(3个384*384),但是和循环无关的Wx不需要稀疏。稀疏过程中保留对角位置的权重,其他位置根据阈值砍掉小权重。
def on_batch_end(self, batch, logs=None):
# print("\n callback batch number", self.batch)
self.batch += 1
# t_start前的batch不进行稀疏,t_start--t_end每interval稀疏一次;t_end之后每个迭代都稀疏
if self.quantize or (self.batch > self.t_start and (self.batch-self.t_start) % self.interval == 0) or self.batch >= self.t_end:
# 针对gru_a层的三个和循环相关重置门,更新门,隐藏状态参数的W_hr,W_hu,W_hs进行稀疏(3个384*384),但是和循环无关的Wx不需要稀疏
layer = self.model.get_layer('gru_a')
w = layer.get_weights()
p = w[1]
# print("gru_a layer p", p.shape)
# p.shape--[384,1152]
nb = p.shape[1]//p.shape[0]
N = p.shape[0]
# print("nb = ", nb, ", N = ", N);
# default:(0.05, 0.05, 0.2)
# print ("density = ", self.final_density)
for k in range(nb):
density = self.final_density[k]
if self.batch < self.t_end and not self.quantize:
r = 1 - (self.batch-self.t_start)/(self.t_end - self.t_start)
density = 1 - (1-self.final_density[k])*(1 - r*r*r)
A = p[:, k*N:(k+1)*N]
# 保留对角位置的权重,其他位置根据阈值砍掉小权重
A = A - np.diag(np.diag(A))
#This is needed because of the CuDNNGRU strange weight ordering
A = np.transpose(A, (1, 0))
# 按4*4块进行稀疏
L=np.reshape(A, (N//4, 4, N//8, 8))
S=np.sum(L*L, axis=-1)
S=np.sum(S, axis=1)
SS=np.sort(np.reshape(S, (-1,)))
thresh = SS[round(N*N//32*(1-density))]
mask = (S>=thresh).astype('float32')
mask = np.repeat(mask, 4, axis=0)
mask = np.repeat(mask, 8, axis=1)
mask = np.minimum(1, mask + np.diag(np.ones((N,))))
#This is needed because of the CuDNNGRU strange weight ordering
mask = np.transpose(mask, (1, 0))
p[:, k*N:(k+1)*N] = p[:, k*N:(k+1)*N]*mask
print("thresh", thresh, np.mean(mask))
if self.quantize and ((self.batch > self.t_start and (self.batch-self.t_start) % self.interval == 0) or self.batch >= self.t_end):
if self.batch < self.t_end:
threshold = .5*(self.batch - self.t_start)/(self.t_end - self.t_start)
else:
threshold = .5
quant = np.round(p*128.)
res = p*128.-quant
mask = (np.abs(res) <= threshold).astype('float32')
p = mask/128.*quant + (1-mask)*p
w[1] = p
layer.set_weights(w)
最后在保存模型参数dump_data时,只保存非0值和索引,非结构化系数矩阵的保存减少.c文件的大小。
LPCNet对于输入2400,3进行了embedding,这里作者对embedding做了一个可微和非整数查找的技巧。对输入信号2400,3通过256,128的矩阵embed,得到2400,3,128--2400,3*128的矩阵。
总结LPCnet的加速技巧:
参考资料:
https://zh-v1.d2l.ai/chapter_recurrent-neural-networks/gru.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。