乐趣区

信号为E时,如何让语音识别脱“网”而出?

欢迎大家前往腾讯云 + 社区,获取更多腾讯海量技术实践干货哦~
本文由腾讯教育云发表于云 + 社区专栏

一般没有网络时,语音识别是这样的

而同等环境下,嵌入式语音识别,是这样的

不仅可以帮您边说边识、出口成章,有个性化名字的时候也难不倒它。
这就是嵌入式语音识别的魅力。
本文将从微信智聆的嵌入式语音识别引擎的实现和优化,
介绍嵌入式语音识别的技术选型。
01
语音识别,大体是这么来的
语音识别,能让机器“听懂”人类的语音,把说话内容识别为对应文本。
开始于上世纪 50 年代
从最初的小词量孤立识别系统
到如今的大词量连续识别系统
语音识别系统的发展,性能得到显著的提升,主要得利于以下几个方面:
大数据时代的到来
深度神经网络在语音识别中的应用
GPU 硬件的发展
因此,语音识别逐步走向实用化和产品化

语音输入法,语音智能助手,语音车载交互系统……
可以说,语音识别是人类征服人工智能的前沿阵地,是目前机器翻译、自然语言理解、人机交互等的奠基石。
然而,性能的提升基于服务端 CPU/GPU 高计算能力和大内存,没有网络的时候将无法享受语音识别的便利。
为了解决这个问题,微信智聆针对嵌入式语音识别进行研发。嵌入式语音识别,也称为嵌入式 LVCSR(或离线 LVCSR,Large Vocabulary Continuous Speech Recognition),指全程运行在手机端的语音识别,而不依赖于服务端强大的计算能力。
在一些网络不稳的特殊场景(车载、境外等等),嵌入式语音识别可“曲线救国”。
那么,实现一个嵌入式语音识别,存在哪些难点呢?

语音识别的基本流程
主流的语音识别算法当中,包括声学和语言两大模型。声学模型得利于近十年深度学习的发展,从 GMM(高斯模型)到 DNN(深度神经网络),再从 DNN 到 LSTM RNN(循环神经网络),识别率不断提升的同时,计算量也不断地飞涨。而语言模型常用的 n -gram 算法,阶数越高性能越好,常用的模型多达数十 G 的内存。
所以综合起来,嵌入式语音识别有以下几个难点:
1. 深度学习运算复杂,仅仅对模型进行裁剪性能损失大,需寻找挽回性能的方法;
2. 裁剪模型不可避免,在模型训练环节如何避免小模型训练易陷入局部最优的问题;
3. 如何计算的更快,满足嵌入式的 CPU 环境;
4. 如何组织语言模型存储,能在有限的内存下存储更多的语言信息。
本文将以语音识别的技术原理出发,浅谈微信智聆嵌入式的实现技术。
内容将分为四个部分:
1. 回顾语音识别的基本概念;
2. 简单介绍在速度和内存优化上我们做的部分工作,侧重于工程应用实现;
3. 说一说为了更好的性能我们做了哪些事,侧重于算法研究介绍;
4. 我们进行实验对比,最后我们进行总结。
02
语音识别的各个组件

语音识别“黑盒”
语音识别从输入录音输出文字,黑盒子处理经过特征提取、声学模型、发音词典、语言模型等流程,笔者认为可以把语音识别比作一台计算机。
特征提取相当于是路由器,作为领头羊给后续环节提供源源不断的数据来源。
声学模型相当于语音识别的心脏——CPU,他将最直接影响着识别的准确性能。
语言模型相当于语音识别的硬盘,大量的词汇组合信息存储于此。
发音词典相当于内存条,能有效组织声学模型与语言模型的关系。
除此之外,语音识别包含一个解码器,他如同计算机的操作系统,有效地组织着各个环节。
接下来,我们基于每个“部件”简介其基本概念,以便后续介绍如何在这些“部件”上对嵌入式 ASR 工作的展开。
1. 特征提取
音识别特征提取包括预加重、分帧、加窗、FFT(Fast Fourier Transform)等一系列流程,常用的特征有 PLP、MFCC、FBANK 等等。一般来说,语音识别把一秒语音分成 100 段(之间有互相重叠),而特征提取能把每段语音数据转化为一个向量(常见的有 39 维 MFCC 特征)。
为了关联上下文信息,特征作为声学模型的输入时,常将相邻帧拼凑一起。比如以 39 维特征为例,前后各取 5 帧信息,那么总共有 11 帧,输入的向量维度为 11*39=429。一般地,语音识别的性能与取帧宽度是正相关的。
作为语音识别的路由器,特征提取环节的运算量并不大。然而其作为声学模型拓扑结构的输入,间接影响着深度学习的运算量,是我们在嵌入式 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 汇编版
汇编版本的 neon 代码编写与维护成本高,但速度比 c 版本更快。秉着精益求精的态度,我们实现了汇编代码:
void vector_product_neon(const char * start_a, const char * start_b, int & result,
const int cnt) {
int res = 0;
asm volatile(
“vmov.s32 q2, #0” “\n\t”
“lsr %[cnt], %[cnt], #4” “\n\t”

“.charloop:”
“vld1.s8 {d0}, [%[vec1]]!” “\n\t”
“vld1.s8 {d1}, [%[vec2]]!” “\n\t”
“vmull.s8 q1, d0, d1” “\n\t”
“vld1.s8 {d0}, [%[vec1]]!” “\n\t”
“vld1.s8 {d1}, [%[vec2]]!” “\n\t”
“vmlal.s8 q1, d0, d1” “\n\t”
“vaddw.s16 q2, q2, d2” “\n\t”
“vaddw.s16 q2, q2, d3” “\n\t”
“subs %[cnt], %[cnt], #1” “\n\t”
“bne .charloop” “\n\t”

“vadd.s32 d4, d4, d5” “\n\t”
“vmov.s32 r4, d4[0]” “\n\t”
“add %[sum], r4” “\n\t”
“vmov.s32 r4, d4[1]” “\n\t”
“add %[sum], r4” “\n\t”

: [sum]”+r”(res)
: [vec1]”r”(start_a),
[vec2]”r”(start_b),
[cnt]”r”(cnt)
: “r4”, “cc”, “memory”
);
result = res;
}
2. 奇异值分解优化声学模型运算量
为了降低乘加运算的次数,我们决定利用奇异值分解来对 DNN 进行重构,通过裁剪掉最小的奇异值及其相对应的特征向量,来达到减少乘加运算数量的目标。奇异值分解将任意矩阵 Wm×n(不失一般性,假设 m≤n) 分解成 3 个矩阵相乘:Wm×n =Um×mΣm×mVm×n。
其中:Σm×m 为对角矩阵, 即 Σm×m =diag(σ1,σ2,…,σm),它的对角元素即为 Wm×n 的奇异值;Um×m 为单位正交矩阵, 其列向量为与奇异值对应的特征向量;Vm×n 中的行向量是互相单位正交的,也是与奇异值对应的特征向量。
下图是我们以 DNN 模型其中一层网络作为例子,阐述我们在重构 DNN 中的模型转化,其中原始 DNN 模型为图中上方子图(a),新重构 DNN 模型在下方子图(b)所示:

a: 原始 DNN 模型的一层结构

(b)新 DNN 模型的两层对应结构
利用 SVD 对声学模型计算量优化大致分为 3 个步骤
(1)训练初始 DNN 神经网络;
(2)对权重矩阵进行奇异值分解;
(3)对重构后的 DNN 模型重新训练。
通过基于 SVD 的模型压缩方法,我们可以在稍微降低模型性能的前提下,将声学模型计算量减少 30%。
3. 哈夫曼优化语言模型内存
一般地,n-gram 语言模型可以用一张有向图存储便于介绍存储空间以及快速查询,这张图上的边要存储词汇信息。我们知道以汉语为例,不同词语的出现频率相差极大,如果所有词汇的 label id 都用 int 类型存储,那空间的利用率较为低下。
以“我”“要”“吃饭”为例,假设语言模型的词汇频率:我 > 要 > 吃饭,那么我们可以构建图 3 的哈夫曼树,则四个字使用的编号码分别为:我(0),要(10),吃饭(110)

二叉哈夫曼

十六叉哈夫曼树
然而,采用图 4 的二叉树数据结构,一次只能处理 1bit 效率较低,也不便于工程实现。所以在工程实现的时候,我们按 4bits 编码为单位,对词汇进行分类存储处理。
我们使用一棵 16 叉树的哈夫曼树结构,每层树节点的编号总量是上一层的 16 倍。树中的所有编号为 0 的子节点用于储存词汇,越高频的词汇储存于深度越低的节点位置。
通过哈夫曼优化,我们的引擎最终成功降低了 25% 的内存占用,同时引擎是资源文件也得到 50% 左右的优化。
04
识别性能的优化
1. 基于 TDNN 优化声学模型
近几年,TDNN(Time-Delay Neural Network,延时神经网络)【5】的拓扑结构被应用于语音识别。事实上,该结构于 1989 年被提出,随着近几年技术的发展,重新进入了大家的视线。

DNN 结构
DNN 的拓扑网络仅针对单一特征时刻点建模。

TDNN 结构
TDNN 的隐层结构,对语音特征多个时刻点进行抽象建模,拥有更强的建模能力。除此之外,TDNN 结构的多时刻建模参数是共享的(图中红、绿、紫用的是同样的拓扑矩阵传播)。
所以,TDNN 虽然在训练的时候,比 DNN 需要更多的 BP 运算。而在语音识别时,由于参数共享的原因,隐层的计算结果可以复用,每一帧仅需对所有参数进行一次运算,大大节省了计算量。最后,我们基于 TDNN 结构,引擎在保持计算量一致的前提下,识别率提升了相对 20% 的准确率。
2. 基于多任务训练优化性能
采用多任务联合训练,能有效提高声学训练的鲁棒性,避免过早陷入局部最优。在嵌入式的模型中,模型输出目标比较少,训练容易陷入局部最优。所以我们,同时用目标多的大模型联合训练,让训练的隐层结构更为鲁棒。

声学模型多任务训练
在训练的时候,我们网络同时拥有输出 1 和输出 2 两个,多任务训练时,逆向迭代需要残差协调,我们采用以下公式分配残差,其中 λ 权衡两个模型的训练权重:

最终我们采用多任务训练优化性能,对语音识别率带来了一定提升,接下来所有的性能提升我们将在下一章结实验给出。
3. 基于区分性训练(Discriminative Training)性能优化
声学模型区分性训练是针对 MLE 训练的不足而提出的。DT 训练通常定义一个目标函数(Objective Function),或者说是准则函数(Criterion Function),来近似一个与分类代价相关的度量。通过区分性训练,我们可以从一定程度上弱化模型假设错误所带来的影响。
同时,由于区分性训练致力于优化与识别效果好坏相关的度量,因此也就为提高识别器性能提供了更直接的途径。形象的说,MLE 训练告诉模型“这是椅子,那是桌子”,而区分性训练则告诉模型“这是桌子而不是椅子,那是椅子而不是桌子”。MLE 训练更重视调整模型参数以反映训练数据的概率分布,而区分性训练则更重视调整模型之间的分类面,以更好的根据设定的准则对训练数据进行分类。
DT 的目标函数是这样的:

对 DT 的目标函数用一次贝叶斯公司可以得到:

分子正是 ML 的目标函数;而分母则是所有文本(包括训练文本和它的所有竞争者)产生训练语音的概率的(按语言模型加权的)和。由于分母上要枚举所有可能的文本并不现实,所以实际中,一般是用一个已有的 ML 训练的语音系别系统对训练语音做一次解码,得到 n -best list 或 lattice,用这里面的文本来近似分母上的求和。n-best list 或 lattice 中包含了训练文本的足够接近的竞争者。
4. 基于互信息的新词发现
对于语音识别系统来说,语言模型对结果影响至关重要;而对于语言模型来讲,语言模型的词典是关键。一个好的分词词典,对于得到鲁棒的语言模型是至关重要的,如果才能选出合理正确的“词”所组成的词典,首先最关键的一步就是基于现有语料的新词挖掘。
由于嵌入式系统性能有限,因此选择合适大小的词表,并对语言模型进行适当剪枝头,可以压缩安装包大小、限制内存消耗、提高识别性能。压缩词表可以筛选高频词,并通过一定的模型来识别筛掉截断词,如“新功”、“嘉年”、“扛生”、“鹅卵”、“刘德”、“利亚”等半个高频词。
一个简单而又有效的新词发现和筛选方案可以采用互信息和左右信息熵的计算方法,计算二元的信息熵的分数由三个对应部分组成:1)点间互信息:点间互信息越高,内部聚合程度越高;2)两个单词片段信息熵 h_r_l 和 h_l_r 的最小值:这个数值越大,则意味着两个单词一起出现的可能性越小;3)单词左右信息熵的最小值:这个数值越大就表示着候选词出现的语境越多,越有可能成词因此,分数越高表示成词的可能性越大。
计算完二元的信息熵后,可以依次计算三元、四元的信息熵,三元的新词发现和筛选是将二元替换原有的两个单字做为一个单字继续进行,候选集可以取左信息熵或者右信息熵为 0 的候选集,四元、五元以此类推。另外,语言模型直接关系到识别结果输出,因此选与应用场景相对应的语料进行统计尤为重要。
05
实验对比
第二章节和第三章节,介绍了一些我们完成的工作,本章节将分为两部分。首先,我们通过实验对比验证工作的成果。其次,我们将引擎和行业竞品进行对比。
工作成果验证
目前总共有 6 个通用测试集,测试集大小分别为 1220、6917、4069、2977、2946、2500 条语音。其中测试集 1 是手机录制测试集,集 2 是命令类的录音,集 3 是麦克风录音涉及一般生活情景,4、5、6 集都是线上实网数据,区别是 集 4、5 背景比较干净,集 6 背景带噪。
测试集
DNN
TDNN
TDNN 优化版
1
10.4
8
6.9
2
13.7
11.3
9.3
3
22.9
18.3
15.6
4
15.8
13.3
12
5
15.3
12.2
10.5
6
22.6
20.3
17.8
在模型选取对比,我们针对 DNN、TDNN、以及 TDNN 优化版(优化内容为第三章的 2、3、4 小结内容),总共设计出三个不同版本的嵌入式语音识别引擎进行对比。
三个版本的嵌入式语音识别引擎在 6 个通用测试集上的实验结果如表中所示。表中的数字表示字错误率,即 100 个字里面识别错字的数量。总体来看,TDNN 对识别率带来了 20% 左右的提升,其他工作也带来了 10% 左右的提升。

从语音识别的基本概念,到语音识别速度和内存优化的介绍,以及沉淀的一些算法研究、实验结果验证,本文大体讲述了语音识别从原理到实践的基本过程。欢迎同样从事语音 AI 识别的小伙伴加入我们~

相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及 CTR 相应知识

退出移动版