语音识别与语音控制





5.00/5 (1投票)
语音识别与语音控制的概念和实现
引言
本文概述了语音识别和语音控制的概念以及它们的实现方式。如今,有多种方法可以使用语音识别,许多人可能通过像 Amazon Echo 这样的云系统来实现。但本文主要关注的是全本地解决方案。两者都有各自的优缺点。
本文将从通过语音命令控制设备的角度来探讨这一主题,通常是通过某种自动化系统来实现的。
背景
这里的任何代码都来自我的 CIDLib
系统,这是一个大型(约 1100 个类,450,000 行代码)的开源项目,托管在 Github 上,您可以在此处找到:
我的 CQC 自动化系统建立在 CIDLib
之上。它利用 CIDLib
提供的语音识别功能来实现“CQC Voice”,这是一个 CQC 的语音控制“前端”,允许用户通过语音命令轻松执行常见操作。
为了更好地理解(和听到)它的实际效果,我还制作了一个相关的 YouTube 视频,链接在此:
语音识别概念
语音识别主要有两种类型。一种是听写,另一种是命令与控制语法。听写顾名思义,用于听写语音内容,以便能够以无需动手的方式“写下来”。它是老式录音笔的高科技版本,人们过去用它来给自己做音频笔记或请秘书转录。现在我们可以直接将音频转换为文本(不总是正确的文本,但它是文本)。
由于听写的使用目的,它必须能够识别相当任意的语音内容。因此,它通常涉及针对每个用户的训练,即他们必须坐下来说一堆话,以便软件能够理解他们如何发音,并且软件需要知道是谁在说话。为了获得良好的识别效果,可能需要进行专门的语音训练。使用该软件的人通常需要相当小心地说话,并清晰地分隔单词。由于这是一项非常棘手的任务,因此出现错误并需要纠正的情况并不少见。
听写在面向语音控制的世界中实际上没有用处。自动化系统很难理解任意语音文本,而且对用户说话方式的限制也太多了。因此,面向语音控制的系统使用另一种样式,即命令与控制,它涉及“语法”。语法由一组“话语”或“规则”组成。它们定义了一组有限的、可以识别的句子。通过允许话语/规则的部分内容是集合中的一个选项,或者可能是少量任意单词,来提供灵活性。它通常带有某种标识符。以下是一些示例:
[SetHouseMode] Put the house into {mode} mode
[SetLightLevel] Set the {light} light to {percent} percent
这只是一个任意的示例语法。每个话语/规则都有一个名称,然后是一些关联的文本。大部分文本是固定的,但它们可以包含“替换令牌”,这些令牌代表用户可以在此处说任意单词或预定选项列表中的单词的位置。因此,在某些系统中,上述示例中的 {mode} 可能有一个预定义的取值集合,而在其他系统中,它可能是用户在此处说的任何单词,当然是在合理的范围内。当语音识别软件匹配成功时,它可能会向自动化系统传递类似以下的内容:
SetLightLevel, light=Kitchen, percent=50
自动化系统只关心这些。它不想陷入解析语音文本并试图理解它的业务。它只需要知道规则/话语的名称和可变部分,以及可能的(非语音的)关联信息(如下文所示)。它将使用这些信息来决定该做什么。
尽管这仍然是一件非常难以做好的事情,但基于语法的方案大大减少了语音识别软件必须处理的可能性数量。即使如此,自动化系统一个合理大小的语法可能仍涉及数十万种可能的单词组合。
语音识别引擎在从传入的音频波形到单词字符串的过程中会经历多个状态。其中大部分是博士论文级别的知识,很遗憾超出了我的理解范围,但它必须进行各种形式的分析,以尝试识别声音,然后将其与单词的一部分进行匹配,然后将其分解成形成单词的块,最后与话语/规则进行匹配。
语音识别语法
上面的示例相当基础,尽管它可能在 Echo 等较简单的系统中使用。对于更全面的语法,它可能会变得相当复杂。在 Windows 上,您可以使用 Speech Platform SDK 来实现语音识别。当然,我已经将该功能封装在 CIDLib
中,使其使用起来更加方便。
Windows 上的语法可以有两种形式,其中一种是 .grxml 文件,它是语法的 XML 表示。这是一个简单的示例,与配套视频中使用的相同:
<?xml version="1.0" encoding="ISO-8859-1"?>
<grammar version="1.0"
xml:lang="en-US"
root="TopRule"
tag-format="semantics/1.0"
xmlns="http://www.w3.org/2001/06/grammar"
xmlns:sapi="http://schemas.microsoft.com/Speech/2002/06/SRGSExtensions"
sapi:alphabet="x-microsoft-ups">
<rule id="TopRule" scope="public">
<item>
<one-of>
<item><ruleref uri="#GoAway"/></item>
<item><ruleref uri="#Shutup"/></item>
</one-of>
</item>
</rule>
<rule id="GoAway">
<item repeat="0-1">
Please
</item>
<item>
go away
</item>
<tag>
out.Action = "GoAway";
out.Type = "Cmd";
out.Target = "Boss";
</tag>
</rule>
<rule id="Shutup">
<item repeat="0-1">
<one-of>
<item>Would you please</item>
<item>Please</item>
</one-of>
</item>
<item>
shut up
</item>
<tag>
out.Action = "Shutup";
out.Type = "Cmd";
out.Target = "MotherInLaw";
</tag>
</rule>
</grammar>
忽略了主要的元素,它只是大量的维护工作,其余内容都在定义语法规则,这是一种 grxml 形式的话语,即规则定义了语法中可识别的句子。对于 grxml,由于它用于相当复杂的语法,因此它非常侧重于将规则分解成可重用的部分,然后以各种方式组合它们,以避免冗余。
因此,您可以定义规则和规则的部分,并在其他规则中引用它们。这本身当然会产生自己的复杂性,但它比大量内容复制要好得多,否则通常会发生这种情况。
在我们的例子中,我们在主元素中开始处理过程,指向顶级规则,我们希望识别引擎从那里开始。
root="TopRule"
在此之后的所有其他内容都是规则的层次结构以及对其他子规则的引用。每个 <rule>
元素都有一个 id
,用于在其他规则中引用它。我们的顶级规则仅引用其他(实际)可供考虑的规则。
顶级规则被标记为“public
”,以便它可以在 .grxml 文件之外可见。这是唯一需要的。如果我们有其他 grxml 文件引用此文件并使用其中的规则,那么它们也必须是公共的。
语法结构
grxml 中的语法结构由各种元素组成,每个元素定义了规则层次结构的一部分。<item>
元素本身是规则的无条件部分,是必须说出的文本片段,才能匹配该规则。但是您也可以为项目提供重复计数,例如说它可以出现 x 到 y 次。最常见的情况是 0 到 1,这意味着它是可选的,例如 <item repeat="0-1">
。
还有一个 <one-of>
,表示必须说出包含的元素之一。这允许您提供用户可以说的变体。这种变体是允许更自然的语法的关键。然而,它也是产生巨大歧义的关键,这使得语音识别引擎更难可靠地将口语句子与规则/话语匹配。每种变体都可能显著增加通过层次结构的可能路径数量。
- one-of 也是前面提到的替换令牌机制的 grxml 版本,我们将在下面详细介绍。
在上面的示例中,我们的主规则表明任何有效的语音命令都必须是我们列出的其他规则之一,在本例中只有两个:
<one-of>
<item><ruleref uri="#GoAway"/></item>
<item><ruleref uri="#Shutup"/></item>
</one-of>
因此,我们两个实际的规则是 GoAway
和 Shutup
。#GoAway
引用此文件中 id
为 GoAway
的另一个规则。如果我们查看实际的 GoAway
规则,它看起来像这样:
<rule id="GoAway">
<item repeat="0-1">
Please
</item>
<item>
go away
</item>
<tag>
out.Action = "GoAway";
out.Type = "Cmd";
out.Target = "Boss";
</tag>
</rule>
所以它有一个可选的前导词“please
”,以及一个必需的短语“go away
”。所以您可以说“go away
”或“please go away
”。
语义信息
规则的其余部分是 <tag>
元素,它定义了“语义信息”,这是该过程的一个关键方面。这是您定义的信息,当用户的语音输入匹配此规则时,该信息将提供给您的程序。这就是您知道发生了什么的方式,它与前面示例中的替换令牌相关。在这种情况下,您传递给您的不是替换令牌,而是您想与命令关联的任何任意信息。
由于 Windows 引擎不允许在话语/规则中使用任意语音文本(像 Amazon Echo 那样),因此最多只能提供规则的可选部分,这样用户就可以说这个或那个选项。但即使这样,您也无法获得这些口语文本。因此,我们必须使用语义值来让应用程序知道匹配了哪个选项。以下是 CQC Voice 中一个子规则的示例,该子规则允许用户说一个媒体播放器传输命令:
<rule id="TransportCmds" scope="private">
<one-of>
<item> pause <tag> out.Cmd = "Pause"; out.Desc = "paused"; </tag> </item>
<item> play <tag> out.Cmd = "Play"; out.Desc = "played"; </tag> </item>
<item> resume <tag> out.Cmd = "Play"; out.Desc = "resumed"; </tag> </item>
<item> restart <tag> out.Cmd = "Play"; out.Desc = "started"; </tag> </item>
<item> start <tag> out.Cmd = "Play"; out.Desc = "started"; </tag> </item>
<item> stop <tag> out.Cmd = "Stop"; out.Desc = "stopped"; </tag> </item>
</one-of>
</rule>
<one-of>
元素中的每个项目代表一个可能的传输命令,并且每个项目都设置了几个语义值。包含对此规则引用的任何更高级别规则都可以访问这些 out.Cmd
和 out.Desc
值。有时,它们会用于设置自己的更高级别语义值。
因此,您可以获得与替换令牌文本方案相同的效果,但仍然只能以静态方式实现,您无法真正地在规则中拥有任意文本,也无法告知说了哪些词。当然,对于某些类型的应用程序,这可能是个交易破坏者。
动态语法
如果您只能拥有完全固定的语法,那将更加受限,但事实并非如此。语法可以是动态的,因为您可以在语法中放置占位符,在运行时,您可以插入您即时生成的实际规则内容,或从配置加载。
例如,在我们的 CQC Voice 中,我们需要知道您的灯、安全区域和媒体播放器的名称等等,以便在您发出命令时知道您指的是什么。我们通过让您告诉我们您有哪些房间以及每个房间里有什么设备来实现这一点。我们利用这些信息在运行时将这些列表插入到整体静态语法文件中,以定制对每个房间的响应。
当然,这会带来更大的歧义问题。当您构建静态语法时,您可以有意识地安排语法来规避最严重的问题。但是当您将任意用户配置的文本插入语法时,通常是不可能的。例如,如果您有一条规则或话语是“将 {light} 灯设置为百分之五十”,如果用户的一盏灯名为“the light”怎么办?
但最终,您必须使用这种动态语法功能来为任何非专为单个应用程序/用户设计的语法提供真实世界的控制语法。
如果您的语法实际上是静态的,您可以对其进行预编译以加快加载速度。否则,您将在应用程序中动态编译它。有关如何执行此操作,请参阅 CIDLib
中的 SpGrammarComp
示例程序。
应用程序交互
当语音识别引擎匹配成功时,它会计算各种规则等的置信度,并向应用程序发出事件。CIDLib
将此转换为一个 TCIDSpeechRecoEv
对象,并将其填充。您可以查看此事件对象的内容来决定该做什么。在某些情况下,您可能什么也不做,因为置信度太低。规则经常会被房间里的对话、电视声音等无意触发。
如果您只是将其中一个输出到文本输出流,您将看到类似以下内容(针对我们上面的 GoAway
规则):
{
RULE: GoAway
BASIC CONF: 0.93
TIME: Wed, Apr 03 18:55:49 2019 -0400
SEMANTIC
{
[0.91] Action=GoAway
[0.91] Type=Cmd
[0.91] Target=Boss
}
RULES
{
[0.93] /GoAway
}
}
因此,我们被告知触发的顶级规则、基本置信度以及时间戳。然后,所有语义信息都与每个语义信息的置信度一起提供。最后,所有构成最终顶级匹配规则的单个规则以及它们的置信度。
因此,您可以看到在这种情况下我们得到了一个好的匹配,置信度在 90% 范围内,清楚地表明我们应该对该事件做出反应。我们将仅使用语义信息来确定用户想要什么,并调用一个方法来执行该命令,在需要根据匹配的特定选项来参数化这些命令时传递语义信息。
在实际应用中,通常会有一个专用线程来监视此类事件。该线程将直接执行命令,或者将它们排队等待其他线程执行实际工作。由于所有命令都来自单个扬声器(即人类扬声器,而不是盒式扬声器),通常会对其进行序列化,尽管有些命令可能会被特殊处理并优先处理,这可能需要通过基于优先级的队列为单独的假脱机和处理线程。
最简单地说,处理循环可能看起来像这样:
TCIDSpeechRecoEv sprecevCur;
tCIDAudStream::EStrmStates eState;
while (!thrThis.bCheckShutdownReq())
{
if (sprecoTest.bGetNextRecEvent(sprecevCur, 250, eState))
m_colProcQ(sprecevCur);
}
它只是等待一段时间的事件。如果出现一个事件,它会将其排队等待其他人处理,然后继续,直到线程被要求关闭。
优点和缺点
可用的语音控制方式各有优缺点。主要在于本地与云端的困境。基于云的系统不受您的控制,取决于可用性,可能随时被中断,或使用条款被更改。当然,总会有各种“锡箔帽”问题,这些问题可能不需要多少“锡箔帽”就可以关注。全本地解决方案由您控制,不依赖任何外部方或系统,并且您说的一切都不会可能被某个大公司获取。
另一方面,基于云的系统拥有最新、最先进的深度神经网络 (DNN) 语音识别技术,这带来了显著的差异。它们非常擅长拒绝背景噪音,并在尖叫的孩子和响亮的电视声中挑选出语音命令。因此,在其他条件基本相同的情况下,它们比本地系统更有可能“正确识别”。
此外,像 Echo 这样的系统带有自己的硬件。语音控制系统通常无法使用单个麦克风,除非它就在您面前。在房间里拾取声音,考虑到其所有反射面和背景噪音,这是一项棘手的任务,因此语音控制系统使用所谓的“麦克风阵列”,它们是小型麦克风的线性或圆形集合。麦克风阵列利用声音到达时间的微小差异来拒绝或减少不感兴趣的声音,增强感兴趣的声音,并忽略反射。由于这是通过声波相位技巧完成的,因此无需物理重新定位麦克风。这几乎是即时发生的。
这些类型的麦克风阵列可供第三方使用,但大多数都针对企业会议室市场,因此不是通用产品。在过去,许多人会使用 Microsoft Kinect 设备。它做了很多事情,但其中之一是它有一个相当不错的麦克风阵列用于此类工作。但 Kinect 已经停产。有一些 DIY 解决方案,但这些解决方案只对极少数潜在的语音控制用户有用,他们愿意拿起烙铁,并且它们的性能可能与商业解决方案相当,也可能不相当。
在某些情况下,您还可以通过使用压缩器和噪声门来大大提高性能,这些设备通常与音乐制作相关。但它们可以通过降低输入信号的动态范围并去除语音命令之间的背景噪音来大大提高成功率。但当然,它们必须经过精心调优,这可能很困难,因为您可能距离阵列一英尺或三十英尺。一些麦克风阵列产品可能内置了其中一种或两种功能,其复杂程度各不相同。
高质量的 DNN 语音控制系统不太可能很快适用于所有本地用途,至少没有适用于自动化系统接口的开放系统,并且价格合理,适合最终用户。此类系统需要海量数据来训练神经网络,这是进入这个领域的一个巨大障碍,因此大多数公司会保守这些优势,并强迫您使用他们的云平台。当然,他们可以利用所有传入的客户命令作为持续的训练素材,进一步完善他们的神经网络,这是本地产品无法获得的优势。
最终,您只能“自选其祸”,俗话说得好。