飞桨paddlespeech语音唤醒推理C定点实现

前面的文章(飞桨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%。说明定点化后性能没有出现明显的下降。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现自然推理系统需要涉及很多知识,包括语言分析、逻辑规则、证明策略等等。这是一个庞大的项目,需要很长时间和精力才能完成。在这里,我只能给你一个大致的框架和思路。 首先,需要定义一些数据结构和函数来表示和处理命题和证明过程。 定义数据结构: ``` // 表示命题的结构体 typedef struct _Proposition { char* formula; // 命题公式 struct _Proposition* next; // 下一个命题 } Proposition; // 表示证明规则的枚举类型 typedef enum { AXIOM, HYPOTHESIS, AND_INTRO, AND_ELIM, OR_INTRO, OR_ELIM, NOT_INTRO, NOT_ELIM, IMPLY_INTRO, IMPLY_ELIM, EQUIV_INTRO, EQUIV_ELIM } Rule; // 表示证明步骤的结构体 typedef struct _ProofStep { Proposition* proposition; // 命题 Rule rule; // 证明规则 struct _ProofStep* prev; // 前一个证明步骤 struct _ProofStep* next; // 后一个证明步骤 } ProofStep; ``` 定义函数: ``` // 创建一个命题 Proposition* createProposition(char* formula); // 销毁一个命题 void destroyProposition(Proposition* prop); // 创建一个证明步骤 ProofStep* createProofStep(Proposition* proposition, Rule rule, ProofStep* prev, ProofStep* next); // 销毁一个证明步骤 void destroyProofStep(ProofStep* step); // 插入一个证明步骤 void insertProofStep(ProofStep* step, ProofStep* before); // 删除一个证明步骤 void deleteProofStep(ProofStep* step); ``` 接下来,需要实现这些函数。 首先是创建和销毁命题的函数: ``` Proposition* createProposition(char* formula) { Proposition* prop = (Proposition*)malloc(sizeof(Proposition)); prop->formula = formula; prop->next = NULL; return prop; } void destroyProposition(Proposition* prop) { if (prop == NULL) { return; } destroyProposition(prop->next); free(prop); } ``` 接下来是创建和销毁证明步骤的函数: ``` ProofStep* createProofStep(Proposition* proposition, Rule rule, ProofStep* prev, ProofStep* next) { ProofStep* step = (ProofStep*)malloc(sizeof(ProofStep)); step->proposition = proposition; step->rule = rule; step->prev = prev; step->next = next; return step; } void destroyProofStep(ProofStep* step) { if (step == NULL) { return; } destroyProposition(step->proposition); destroyProofStep(step->prev); destroyProofStep(step->next); free(step); } ``` 然后是插入和删除证明步骤的函数: ``` void insertProofStep(ProofStep* step, ProofStep* before) { step->prev = before->prev; step->next = before; before->prev->next = step; before->prev = step; } void deleteProofStep(ProofStep* step) { step->prev->next = step->next; step->next->prev = step->prev; destroyProofStep(step); } ``` 接下来就是实现证明策略的部分。证明策略是推理系统的核心,它包括了一系列的推理规则和推理步骤。 下面是一些常见的证明规则: - 假言引入:如果前提是“如果A,那么B”,且已经证明了A,则可以得出B。 - 假言消去:如果已经证明了“A”,且已经证明了“如果A,那么B”,则可以得出B。 - 合取引入:如果已经证明了A和B,则可以得出“A且B”。 - 合取消去:如果已经证明了“A且B”,则可以得出A和B。 - 析取引入:如果已经证明了A,则可以得出“A或B”。 - 析取消去:如果前提是“A或B”,且已经证明了A,那么可以得出结论。同理,如果前提是“A或B”,且已经证明了B,那么也可以得出结论。 在实现证明策略时,需要考虑到各种不同的情况和推理规则的适用条件。这需要涉及到语言分析和逻辑推理的知识,需要进行深入的研究和设计。 最后,需要实现一个主函数,用来调用各种函数进行推理过程的演示。这个主函数需要读入用户输入的命题和证明步骤,并进行相应的处理和推导。 以上只是一个大致的框架和思路,实现自然推理系统需要涉及到很多细节和复杂的设计。如果你有兴趣和时间,可以进一步深入研究和实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值