前面的文章(飞桨paddlespeech语音唤醒推理C浮点实现)讲了飞桨paddlespeech语音唤醒推理的C浮点实现。但是嵌入式设备通常CPU频率低和memory小,在嵌入式设备上要想流畅的运行语音唤醒功能,通常用的是定点实现。于是我就在浮点实现(把卷积层和相应的batchNormal层合并成一个卷积层)的基础上做了定点实现。需要说明的是目前完成的是16bit的定点实现,后面会在此基础上做8bit的定点实现。
做定点实现主要包括两部分工作,一是模型参数的量化和定Q格式等,二是基于Q格式的定点实现。关于模型参数的量化,我曾写过相关的文章(深度学习中神经网络模型的量化),有兴趣的可以去看看。我用的是对称量化,这里简述一下这部分的工作。
1, 在python下根据paddlepaddle提供的API(named_parameters)得到模型每层的参数(weight & bias),同时看每层的weight和bias的绝对值的最大值,从而确定参数的Q格式,再以这个Q格式对weight 和bias做量化。
2, 在python下得到测试集里非常多个文件每层的输入和输出的绝对值的最大值,从而确定每层的输入和输出的Q格式。
至于代码的定点化,主要包括如下几点:
1, 卷积层的定点化
主要是做好乘累加以及输出的移位和防饱和处理。在文章(深度学习中神经网络模型的量化)里有详细描述,这里就不细讲了。
2, sigmoid的定点化
调研了一下,sigmoid的定点化主要用查表法来实现。Sigmoid(x)在x<=-8时近似为0,在x>=8时近似为1,因此做表时在[-8,8)之间就可以了。 若表中有256个值,则表中x的间隔是16/256 = 0.0625。表中第一个值对应的是x=-8时sigmoid的值,第二个值对应的是x=-7.9375(-8 + 0.0635 = -7.9375)时sigmoid的值,以此类推。Sigmoid输出的取值范围是(0,1),因此用的Q格式是Q0.15。例如当x=0时,sigmoid(0) = 0.5,表示成Q0.15格式是0x4000。当x在[-8,8)范围内每隔0.0625的256个sigmoid值都算出来并换算成Q0.15格式,就得到表中的256个值了。
具体实现时参考率CMSIS_5的代码,如下图:
做表时把前128个值(x < 0时的)与后128个值(x>=0时的)做了位置上的互换。主要是因为处理时先对x定点化后的16位输入值做右移8位处理,就变成了8位的值,再变成unsigned char(U8)用于做表的索引。 U8(0) = 0, U8(127) = 127, 但U8(-128) = 128, U8(-127) = 129, ……, U8(-1) = 255。所以表中的位置前后部分就互换了。再看sigmoid层的输入与sigmoid函数的输入的关系。 假设sigmoid函数输入的16位定点值为0x1869,右移8位后为0x18,即为24。表中第24个代表的是x=1.5(24 * 0.0675 = 1.5)时的sigmoid值。我的sigmoid层的输入Q格式是Q7.8, 1.5用Q7.8表示就是0x0180, 而函数中要求的是0x18XX,所以需要把层的输入的值做左移4位处理。由于sigmoid函数只对[-8,8)内的值做处理,因此首先需要对层的输入值做[-8,8)的限幅处理。上面两步的代码如下图:
调sigmoid_q15()时把int_width设成3,就表示输入范围是[-8,8)。 由于输入的x值不一定正好落在表中的那些点上,如x = 0.0325就落在点0.0和点0.0625之间。 为了使sigmoid的输出值更准确,函数中用线性插值法求那些不落在点上的sigmoid值。我在文章(基于sinc的音频重采样(二):实现)中讲过线性插值法,有兴趣的可以去看看。要想sigmoid的输出值更准确,还可以扩大表里值的个数,比如变成512个值,代价是多用些memory。
3, 确定好评估的指标
我在文章(深度学习中神经网络模型的量化)中对评估指标有所描述。这里我选用的是欧氏距离(Euclidean Distance)。具体调试时浮点实现和定点实现并行运行。即算出的浮点的fbank值作为浮点实现模型的输入,将浮点的Fbank值根据定标转换成定点值作为定点实现模型的输入,然后每层的浮点实现和定点实现并行运行。浮点实现得到的结果是浮点值,定点实现得到的结果是定点值,再根据输出的Q格式转换成浮点值。最后再用欧氏距离对输出结果进行评估。下图给出了某一depthwise卷积层的实现代码。先做浮点的卷积层运算,结果保存在fbankFloat里,然后做定点的卷积层运算,结果保存在fbankFix里,再根据输出的Q格式将fbankFix转换成浮点值,最后算欧氏距离。欧氏距离越小越好。
下图给出了调试好后部分层的欧氏距离的值,都是很小的(图中0/1/2等表示卷积层ID)。
4,如何调试
模型定点化调试时要从第一层到最后一层一层一层的调试,只有当上一层的欧氏距离达标后再去调下一层。具体到调试某一层时,通过log找到那些浮点值与定点转浮点后的值差值较大的值,再到浮点实现和定点实现里打印出输入和运算后的具体值,分析具体原因。有可能是定点实现里移位防饱和等没做好,也有可能是参数量化没作对,还有可能是输入和输出的Q格式没定好导致误差偏大等。在定输入和输出的Q格式时,是根据绝对值的最大值来的。如果发现精度不够,有可能需要调整输入或输出的Q格式(小数位要多一位,依据是看超出定标最大值出现的次数,次数占比较小就可以)。
调试时是用一个音频文件去调。等模型调试完成后要在一个大的数据集上对定点实现做全面的评估,看唤醒率和误唤醒率的变化。我做完定点实现后在一个有两万五千多音频文件的数据集上做评估,跟浮点实现比,唤醒率下降了0.2%,误唤醒率上升了0.3%。说明定点化后性能没有出现明显的下降。