深度学习基础知识六--LPCNet之GRU稀疏化

上文介绍了LPCNet的算法原理和工程,本文主要介绍LPCNet的加速方案之稀疏化处理。

我们首先了解GRU,然后再看作者如何对GRU进行稀疏化,来提升网络性能。

GRU(门控循环单元)流程

GRU的整个流程如下图所示:

重置门和更新门:重置门和更新门的输入为当前时刻输入X_t 和上一个时刻隐藏状态H_{t-1} ,通过全连接层和激活层得到输出

R_tZ_t ,sigmoid激活所以值域0,1。

候选隐藏状态:当前时刻R_t 和上一时刻隐藏状态做乘法,结果和当前时刻输入结合,通过全连接和激活得到当前时刻候选隐藏状态,激活为tanh,s所以值域[-1,1]。

\widetilde{H_t}= tanh(X_tW_{xh} + (R_t \cdot H_{t-1})W_{hh} + b_h)

隐藏状态:最后,时刻t的隐藏状态是由当前时刻的Z_t 结合上一时刻的隐藏状态和当前时刻的候选隐藏状态组合得到。

H_t = Z_{t} \cdot H_{t-1} + (1-Z_t) \cdot \widetilde{H_t}

以上就是GRU的流程,其中W为权重参数,B为偏差参数,具体参数量我们在下一节详细介绍。

GRU(门控循环单元)实现

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(门控循环单元)参数量

了解了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的加速技巧:

  1. 采样网络一帧计算一次
  2. embed_sig使用embed将cpcm信号(1*3)embed到1*384
  3. gru和循环无关的与W_{xr},W_{xz},W_{xh}矩阵运算提前合并计算好,因为X要先嵌入再和W_x相乘,可以将两个操作一起计算
  4. 与循环相关的W_{wr},W_{wz},W_{wh}进行了稀疏化,dump_data时只保存非0值和索引

参考资料:

https://zh-v1.d2l.ai/chapter_recurrent-neural-networks/gru.html

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...