作为语音识别的路由器,特征提取环节的运算量并不大。然而其作为声学模型拓扑结构的输入,间接影响着深度学习的运算量,是我们在嵌入式ASR中要考虑的问题。
2.帧率抖动
5s统计一次直播流视频帧率,1min计算一次帧率方差,方差过大,视为推流帧率抖动.
3.声学模型(acoustic model)
声学模型作为语音识别的CPU,其重要性不言自喻。
一般地,它占据着语音识别大部分的运算开销,直接影响着语音识别系统的性能。传统语音识别系统普遍基于GMM-HMM的声学模型,其中GMM对语音声学特征的分布进行建模,HMM则用于对语音信号的时序性进行建模。
2006年深度学习兴起以后,深度神经网络(DNN,Deep Neural Networks)被应用于声学模型。
近十多年,声学模型的上深度学习的发展一路高歌,各种CNN、RNN、TDNN的拓扑结构如雨后春笋一一冒出,关于深度学习在声学模型的更多介绍见文。
对于嵌入式LVCSR来说,选择合适的DNN拓扑结构,并用合理的优化在手机实现结构的运算,是声学模型在其的核心诉求。
4.语言模型(language model)
语言模型,NLP从业者相对更为熟悉。在语音识别里,语言模型用来评估一个句子(即图2的词语序列)出现的概率高低。
在语言模型的实现算法中,最常见的为n-gram模型(n-gram models),利用当前词前面的n个词来计算其概率,是一个上下文有关模型。几年来,神经语言模型(Neural language models)使用词汇Embedding来预测,也得到广泛的发展与应用。
在嵌入式ASR中,由于计算资源要留予声学模型,所以语言模型采用的依旧是n-gram的思想。那么在有限的内存中,如何最大化存储语言模型,是嵌入式ASR要解决的问题。
5.发音词典
发音词典,是语音识别的内存条。内存能将硬盘的数据读入,并使用cpu进行运算。同样的,发音词典,能将语言模型的词条序列转化为音素序列,并用声学模型进行分数评估运算。
发音词典是连接声学模型和语言模型的桥梁,他的大小直接影响声学模型和语言模型的发挥空间。
在嵌入式ASR中,发音词典的大小,与语言模型的规模互相共鸣,所以要解决的问题可以与语言模型归为一谈。
6.解码器
解码器,估计这个词的来自英文decoder的直译,笔者认为更恰当的名字应称为识别器。之所以叫解码器,还有另外一个比较形象的原因。以16bit语音数据为例,计算机的存储是一堆我们看不懂的short类型数字,如同密码一般。语音识别能破解这些密码,将明文展示在我们面前。
所以通俗来讲,解码器就是将语音识别各个流程串联的代码工程。一般云端采用与WFST(带权优有限状态自动机)搭档的静态解码器,可以更方便地综合处理语音识别的各个环节。而嵌入式为了节省语言模型的内存开支,采用特定的动态解码器。
03
开始优化这些组件——速度和内存优化
为了优化这些“部件”占用的时间与内存,我们做了一系列工作:
neon计算优化,奇异值分解优化,哈夫曼编码优化。
1.neon优化声学模型计算
neon的计算优化,已是广大工程师们的老生常谈,机器学习相关的T族们更是耳熟能详。在嵌入式ASR引擎中,我们对核心高频运算的函数进行了neon优化,采用了汇编语言进行编写,最终有效提高了25%的计算速度。
接下来,本文现以实现char类型向量乘的介绍优化的实现,分三版本来介绍:
A. 优化前的朴素版
B. neon c版
C. neon汇编版
首先,我们将要实现的函数是:
/** * 实现两个char类型向量乘 * start_a: 向量A * start_b: 向量B * cnt:向量元素个数 * result:向量乘返回存储变量 */ void vector_product_neon(const char * start_a, const char * start_b, int & result, const int cnt);A. 优化前朴素版
void vector_product_neon(const char * start_a, const char * start_b, int & result, const int cnt) { int res = 0; for(int j = 0; j < cnt; j++) { res += int(*start_a) * int(*start_b); start_a++; start_b++; } result = res; }B. neon c版
Neon寄存器能实现128位空间的并行运算,对于char类型的向量乘而言,两两相乘的结果在short类型范围内,故可8个为一组实现。以下代码,8个元素一组,一次循环处理两组。在我们的深度学习运算中,隐层的向量长度保证为16倍数,实现代码如下:
void vector_product_neon(const char * start_a, const char * start_b, int & result, const int cnt) { int res = 0; int32x4_t neon_sum = vdupq_n_s32(0); int8x8_t neon_vector1; int8x8_t neon_vector2; for(int j = 0; j < cnt / 16; j++) { neon_vector1 = vld1_s8((char *)start_a); neon_vector2 = vld1_s8((char *)start_b); int16x8_t neon_tmp = vmull_s8(neon_vector1, neon_vector2); start_a += 8; start_b += 8; neon_vector1 = vld1_s8((char *)start_a); neon_vector2 = vld1_s8((char *)start_b); neon_tmp = vmlal_s8(neon_tmp, neon_vector1, neon_vector2); neon_sum = vaddw_s16(neon_sum, vget_low_s16(neon_tmp)); neon_sum = vaddw_s16(neon_sum, vget_high_s16(neon_tmp)); start_a += 8; start_b += 8; } for(int j = 0; j < 4; j++) res += vgetq_lane_s32(neon_sum, j); result = res; }C. neon汇编版