微软认知服务学习指南(二)
原文:
annas-archive.org/md5/d574e5ef7c17b4f651431caa0b5defbd译者:飞龙
第三章:分析视频
在上一章中,我们探讨了用于处理图像的不同 API。我们将介绍一个新的 API:Video Indexer API。
在本章中,我们将涵盖以下主题:
-
Video Indexer 的一般概述
-
使用预构建 UI 的 Video Indexer 指南
深入了解 Video Indexer
Video Indexer 是一种服务,允许您上传视频并从您上传的视频中获得洞察。这些洞察可用于使视频(以及由此扩展的内容)更容易被发现。它们还可以用于提高用户参与度。
一般概述
通过使用人工智能技术,Video Indexer 使您能够提取大量信息。它可以从以下列表中的功能中获得洞察:
-
带语言检测的音频字幕
-
创建字幕
-
噪音降低
-
人脸跟踪和识别
-
说话人索引
-
视觉文本识别
-
语音活动检测
-
场景检测
-
关键帧提取
-
情感分析
-
翻译
-
视觉内容审核
-
关键词提取
-
注释
-
品牌检测
-
物体和动作标注
-
文本内容审核
-
情感检测
典型场景
以下列表显示了一些可能希望使用 Video Indexer 的典型场景:
-
搜索:如果您有一个视频库,您可以使用 Video Indexer 获得的洞察来索引每个视频。通过(例如)语音或两个特定人物同时出现的位置进行索引可以提供更好的搜索体验。
-
货币化:通过使用 Video Indexer 获得的洞察,可以提高每个视频的价值。例如,您可以通过使用视频洞察来展示上下文正确的广告,从而提供更相关的广告。例如,通过使用洞察,您可以在足球比赛中而不是游泳比赛中展示运动鞋广告。
-
用户参与度:通过使用 Video Indexer 获得的洞察,您可以通过显示视频的相关元素来提高用户参与度。如果您有一个涵盖 60 分钟不同材料的视频,在该时间段内放置视频时刻允许用户直接跳转到相关部分。
关键概念
以下部分描述了在讨论 Video Indexer 时重要的关键概念。
拆解
拆解是一个包含所有洞察详细信息的完整列表。这是完整视频字幕的来源;然而,拆解通常过于详细,不适合用户。相反,您通常希望使用总结性洞察来仅获取最相关的知识。如果需要更详细的洞察,您将从一个总结性洞察转到完整的拆解。
总结性洞察
而不是检查数千个时间范围并查找给定数据,可以使用总结性洞察。这将为您提供数据的聚合视图,例如面孔、关键词和情感,以及它们出现的时间范围。
关键词
从视频中的任何转录音频中,视频索引器将提取可能与视频相关的关键词和主题列表。
情感
当视频被转录时,它也会进行情感分析。这意味着您可以判断视频是更积极还是更消极。
块
块用于轻松地通过数据。如果有说话者更改或音频之间的长暂停,这些可能被索引为单独的块。
使用视频索引器解锁视频洞察
在本节中,我们将探讨如何使用视频索引器。
如何使用视频索引器
我们将快速查看您如何利用视频索引器。
通过网页门户
要使用微软提供的预构建视频索引器工具,请访问vi.microsoft.com/。使用您的微软账户注册或登录。登录后,您将需要填写一些信息以注册账户,如下面的截图所示:
一旦您登录,您将发现自己在一个仪表板上,如下面的截图所示:
要开始,您可以通过点击上传来上传您的视频。这将打开一个弹出窗口,您可以使用它上传视频或输入视频的 URL。或者,您可以通过在菜单中点击示例视频来快速选择一个示例视频。
当您选择了一个视频,或者您上传的视频已完成索引时,您将被带到一页来查看洞察。此页面将显示视频的全部内容,以及找到的任何洞察,如下面的截图所示:
除了在视频中发现的任何关键词和人之外,您还将获得视频中的语音注释和情感列表。这些洞察将提供以下信息列表(如果检测到此类信息):
-
视频中出现的人物
-
关于视频内容的关键词
-
与视频相关的标签
-
检测到的品牌
-
情感
-
关键帧列表
视频索引器还将创建视频中每个关键事件的时序表。您可以通过在洞察框架顶部选择时序来跟踪此时序表,如下面的截图所示:
此时序表将随着视频的播放自动向前移动。
时间线将显示视频中的任何音频的文本。此外,它还将显示检测到的任何对象和识别到的人物。
视频索引器 API
除了预制的视频索引器网站外,还有一个视频索引器 API 存在。这允许您从自己的应用程序中获得与网络工具完全相同的洞察力。
要开始使用 API,请访问api-portal.videoindexer.ai/。到达此处后,使用你的 Microsoft 账户登录。第一步是订阅 API 产品。你可以通过点击产品标签来完成此操作。这将显示以下内容:
点击授权,你将被带到另一个页面,如下面的截图所示:
点击添加订阅。这将显示以下内容:
填写订阅名称并确保你已阅读并同意使用条款。点击确认。
一旦你订阅了产品,你将被带到查看 API 密钥的页面,如下面的截图所示。这可以通过转到产品标签并选择你订阅的产品来始终访问:
一旦你获得了密钥,选择APIs标签并选择你的订阅产品。这将显示所有可供你使用的 API 调用。整个 API 都是基于 REST 的,因此只要你提供正确的请求参数和 API 密钥,你就可以从任何应用程序中使用它。
摘要
在本章中,我们介绍了视频索引器。我们从概述开始,了解什么是视频索引器。然后我们学习了如何在视频索引器 Web 应用程序中分析视频。我们通过查看如何注册 REST API 来结束本章,使我们能够在我们自己的应用程序中利用视频索引器的力量。
在下一章中,我们将从视觉 API 转向第一语言 API。你将学习如何利用 LUIS 的力量配置 API 以理解句子中的意图。
第四章:让应用程序理解命令
“LUIS 在从原型到生产的过程中为我们节省了大量的时间。”
- Eyal Yavor,Meekan 的联合创始人兼 CTO
在前面的章节中,我们专注于视觉 API。从本章开始,我们将转向语言 API,我们将从语言理解智能服务(LUIS)开始。在本章中,您将学习如何创建和维护语言理解模型。
到本章结束时,我们将涵盖以下主题:
-
创建语言理解模型
-
使用 Bing 和 Cortana 预构建模型处理常见请求
创建语言理解模型
有时,我们可能希望我们的电脑能理解我们的需求。在我们日常的业务中,我们希望能够用常规句子与电脑或手机交谈。没有额外的帮助,这是很难做到的。
利用 LUIS 的力量,我们现在可以解决这个问题。通过创建语言理解模型,我们可以让应用程序理解用户的需求。我们还可以识别关键数据,这通常是您希望成为查询或命令一部分的数据。如果您正在询问某个问题的最新新闻,那么关键数据就是您所询问的新闻的主题。
创建应用程序
要开始使用 LUIS,你应该前往www.luis.ai。这是我们设置应用程序的地方。点击登录或创建账户按钮开始。
让我们创建我们的第一个应用程序。从顶部菜单点击我的应用。这应该会带您回到应用程序列表,列表应该是空的。点击新建应用。
在显示的表格中,我们填写有关我们应用程序的信息。我们需要给应用程序起一个名字。我们还需要指出一个非典型使用场景,默认设置为其他(请指定)。相反,将其设置为SmartHouseApplication。此应用程序属于工具领域。我们将选择英语应用程序文化。
可用的其他语言包括巴西葡萄牙语、中文、法语、德语、意大利语、日语和西班牙语。
下面的截图显示了我们可以如何定义应用程序:
当你点击创建按钮时,应用程序将被创建。这个过程大约需要一分钟或更长时间来完成,所以请耐心等待。
当应用程序创建完成后,您将被带到应用程序的主页,如下面的截图所示:
如您所见,我们有各种功能可以使用,我们将在下面介绍重要的功能。
我们将要构建的应用程序将针对我们的智能家居应用程序。我们将配置应用程序以识别设置不同房间温度的命令。此外,我们希望它能告诉我们不同房间的温度。
使用实体识别关键数据
LUIS 的一个关键特性是能够识别句子中的关键数据。这些关键数据实例被称为实体。在一个新闻应用程序中,实体的一个例子是主题。如果我们要求获取最新的新闻,我们可以指定一个主题供服务识别。
对于我们的应用程序,我们想要添加一个关于房间的实体。我们通过在左侧面板中选择实体来实现这一点。然后我们点击添加自定义实体。
我们将看到以下屏幕:
输入实体的名称并点击保存按钮。就这样——您现在已经创建了第一个实体。我们将在稍后看到如何使用它。
如您可能已注意到,在实体创建表单中有一个名为实体类型的下拉列表。实体类型是一种创建层次实体的方式,这基本上是关于定义实体之间关系的问题。
例如,您可以想象在给定的时间范围内搜索新闻。通用的顶级实体是日期。从那里开始,您可以定义两个子项,StartDate和EndDate。这些将由服务识别,其中将为实体及其子项构建模型。
要添加一个层次子实体,请勾选复选框并从选择中选取层次。为每个要添加的子项,点击实体子项旁边的+按钮,如图所示。输入子项的名称:
您可以添加的其他类型的实体被称为组合实体。这是一种由一组现有实体形成的实体类型。这就是我们所说的“具有”关系,因此组件是子项,但不是在父子关系中。
组合实体与层次实体不共享共同特征。当删除顶级实体时,不会删除组件。使用组合实体,LUIS 可以识别实体组,然后将其作为单个实体处理。
使用组合实体就像订购披萨一样。您可以通过说“我想一个大披萨,上面有蘑菇和意大利辣肠”来订购披萨。在这个例子中,我们可以看到大小作为一个实体,我们也可以看到两种配料作为实体。将这些组合起来可以形成一个组合实体,这被称为订单。
您可以添加的最后一种实体类型被称为列表实体。这是一个用于在话语中作为关键词或标识符使用的自定义实体值列表。
当使用实体时,有时一个实体可能由多个单词组成。在我们的例子中,对于Rooms实体,我们可能要求客厅。为了能够识别这样的表述,我们可以定义一个特征列表。这是一个以逗号分隔的列表,可以包含一些或所有预期的短语。
让我们为我们的应用程序添加一个。在左侧,面板底部,您将看到Features(功能)。选择此选项,然后点击Add phrase list(添加短语列表)来创建一个新的列表。将其命名为Rooms,并添加您预期在房屋中找到的不同房间,如下面的截图所示:
通过点击右侧的Recommend(推荐),LUIS 将推荐与您已输入的相关更多值。
我们将在稍后看到这是如何被利用的。
除了创建短语列表,我们还可以创建模式特征。使用模式特征的典型用例是当您有符合模式的数据,但无法将其作为短语列表输入时。模式特征通常与产品编号一起使用。
使用意图理解用户的需求
现在我们已经定义了一个实体,是时候看看它是如何与意图相匹配的了。意图基本上是句子的目的。
我们可以通过在左侧面板中选择Intents(意图)选项来向我们的应用程序添加意图。点击Add intent(添加意图)。当我们添加意图时,我们给它一个名字。名字应该描述意图的内容。我们想要添加一个名为GetRoomTemperature的意图,其目的是获取特定房间的温度,如下面的截图所示:
当您点击Save(保存)按钮时,您将被带到语句页面。在这里,我们可以添加用于意图的句子,所以让我们添加一个。输入厨房的温度是多少?然后按Enter。这个句子(或称为语句)将准备好进行标记。标记语句意味着我们定义它属于哪个意图。我们还应该确保用正确的类型标记实体。
以下截图显示了我们的第一个语句的标记过程:
如您所见,实体已被标记。您可以通过点击单词来告诉 LUIS 一个单词是特定的实体。这将弹出一个包含所有可用实体的菜单,然后您可以从中选择正确的一个。同时,注意在下拉列表中如何选择GetRoomTemperature意图。完成标记您的语句后,点击Train。
所有应用程序都是使用默认的意图None创建的。这个意图将包括不属于我们应用程序的句子。如果我们说要订购一个带蘑菇和意大利辣肠的大披萨,这将导致意图为None。
当你创建意图时,你应该定义至少三到五个话语。这将给 LUIS 一些可以工作的事情,因此它可以创建更好的模型。我们将在本章后面看到我们如何提高性能。
使用预构建模型简化开发
构建实体和意图可以是简单的,也可以是复杂的。幸运的是,LUIS 提供了一套来自 Bing 的预构建实体。这些实体将包含在应用程序中,以及在网上,在经过标签化过程时。
以下表格描述了所有可用的预构建实体:
| 实体 | 示例 |
|---|---|
builtin.number | 五,23.21 |
builtin.ordinal | 第二,第三 |
builtin.temperature | 2 摄氏度,104 华氏度 |
builtin.dimension | 231 平方公里 |
builtin.age | 27 岁 |
builtin.geography | 城市,国家,兴趣点 |
builtin.encyclopedia | 人物,组织,事件,电视剧集,产品,电影等 |
builtin.datetime | 日期,时间,持续时间,设置 |
最后三个有多个子实体,如表格中示例列所述。
我们将添加这些预构建实体中的一个,因此请从菜单中选择实体。点击添加预构建实体,从列表中选择温度,然后点击保存。
使用新创建的实体,我们想要添加一个名为SetTemperature的新意图。如果示例话语是将厨房的温度设置为 22 摄氏度,我们可以如以下截图所示标注话语。
如您所见,我们有一个room实体。我们还有一个清晰标注的预构建temperature实体。由于正确的意图应该在下拉菜单中选择,我们可以点击训练按钮来保存话语。
预构建域
除了使用预构建实体外,我们还可以使用预构建域。这些是已经存在的实体和意图,利用了来自不同域的常用意图和实体。通过使用这些意图和实体,你可以使用通常在 Windows 中使用的模型。一个非常基本的例子是在日历中设置约会。
要使用 Cortana 的预构建域,你可以从左侧菜单中选择预构建域。这将打开一个可用域的列表。通过点击添加域,你可以添加选定的域,如以下截图所示:
这将添加该特定域的意图和实体到已定义的意图和实体列表中,如以下截图所示:
以下列表显示了 Cortana 预构建域中可用的顶级域。有关可用的预构建域的完整列表,请参阅附录 A,LUIS 实体:
-
日历 -
相机 -
通信 -
娱乐 -
事件 -
健身 -
游戏 -
智能家居自动化 -
电影票 -
音乐 -
备注 -
设备端 -
地点 -
提醒 -
餐厅预订 -
出租车 -
翻译 -
公用事业 -
天气 -
网络
训练模型
现在我们有一个工作的模型,是时候将其投入使用了。
训练和发布模型
使用该模型的第一步是确保模型有一些话语可以处理。到目前为止,我们为每个意图添加了一个话语。在我们部署应用程序之前,我们需要更多。
想想三种或四种设置或获取房间温度的不同方法,并将它们添加进去,指定实体和意图。此外,添加一些属于None意图的话语,仅作参考。
当我们添加了一些新的话语后,我们需要训练模型。这样做将使 LUIS 开发代码来识别未来的相关实体和意图。这个过程是定期进行的;然而,在发布之前,在您做出更改时进行此操作是明智的。这可以通过在顶部菜单中点击训练来完成。
要测试应用程序,您可以在交互式测试选项卡中简单地输入测试句子。这将显示任何给定句子是如何被标记的,以及服务发现了哪些意图,如下面的截图所示:
训练完成后,我们可以发布应用程序。这将部署模型到 HTTP 端点,该端点将解释我们发送给它的句子。
从左侧菜单中选择发布。这将显示以下屏幕:
点击发布按钮来部署应用程序。端点 URL 设置字段下的 URL 是模型部署的端点。如您所见,它指定了应用程序 ID 以及订阅密钥。
在我们继续前进之前,我们可以验证端点是否实际工作。您可以通过在文本字段中输入一个查询(例如,获取卧室温度)并点击链接来完成此操作。这应该会向您展示以下类似截图的内容:
当模型发布后,我们可以继续通过代码访问它。
连接到智能家居应用程序
为了能够轻松地与 LUIS 一起工作,我们将想要添加 NuGet 客户端包。在智能家居应用程序中,转到 NuGet 包管理器并找到Microsoft.Cognitive.LUIS包。将此包安装到项目中。
我们需要添加一个名为Luis的新类。将文件放在Model文件夹下。这个类将负责调用端点并处理结果。
由于我们需要测试这个类,我们需要添加一个View和一个ViewModel。将LuisView.xaml文件添加到View文件夹中,并将LuisViewModel.cs添加到ViewModel文件夹中。
View应该相当简单。它应该包含两个TextBox元素,一个用于输入请求,另一个用于显示结果。我们还需要一个按钮来执行命令。
将View作为TabItem添加到MainView.xaml文件中。
ViewModel应该有两个string属性,分别对应于两个TextBox元素。它还需要一个ICommand属性用于按钮命令。
我们首先创建Luis类,因此打开Luis.cs文件。将类设置为public。
当我们发出请求并收到相应的结果时,我们希望触发一个事件来通知 UI。我们希望这个事件带有一些额外的参数,因此,在Luis类下面创建一个名为LuisUtteranceResultEventArgs的类,该类继承自EventArgs类,如下所示:
public class LuisUtteranceResultEventArgs : EventArgs {
public string Status { get; set; }
public string Message { get; set; }
public bool RequiresReply { get; set; }
}
这将包含一个Status字符串,一个Message状态,以及Result本身。回到Luis类,添加一个事件和一个私有成员,如下所示:
public event EventHandler<LuisUtteranceResultEventArgs> OnLuisUtteranceResultUpdated;
private LuisClient _luisClient;
我们已经讨论了事件。私有成员是 API 访问对象,我们从 NuGet 安装了它:
public Luis(LuisClientluisClient) {
_luisClient = luisClient;
}
构造函数应接受LuisClient对象作为参数,并将其分配给之前创建的成员。
让我们创建一个辅助方法来触发OnLuisUtteranceResultUpdated事件,如下所示:
private void RaiseOnLuisUtteranceResultUpdated( LuisUtteranceResultEventArgsargs)
{
OnLuisUtteranceResultUpdated?.Invoke(this, args);
}
这纯粹是为了我们自己的方便。
为了能够发出请求,我们将创建一个名为RequestAsync的函数。这个函数将接受一个string作为参数,并返回Task类型。该函数应标记为async,如下所示:
public async Task RequestAsync(string input) {
try {
LuisResult result = await _luisClient.Predict(input);
在函数内部,我们调用_luisClient的Predict函数。这将向之前发布的端点发送查询。成功的请求将导致一个包含一些数据的LuisResult对象,我们将在稍后探讨。
我们在一个新函数中使用结果并处理它。我们确保捕获任何异常,并使用以下代码通知任何监听者:
ProcessResult(result);
}
catch(Exception ex) {
RaiseOnLuisUtteranceResultUpdated(new LuisUtteranceResultEventArgs
{
Status = "Failed",
Message = ex.Message
});
}
}
在ProcessResult函数中,我们创建一个LuisUtteranceResultEventArgs类型的新对象。当通知监听者任何结果时将使用此对象。在这个参数对象中,我们添加Succeeded状态和result对象。我们还输出一个消息,说明顶级识别的意图。我们还添加了这个意图是所有意图中顶级意图的可能性。最后,我们还添加了识别到的意图数量:
private void ProcessResult(LuisResult result) {
LuisUtteranceResultEventArgsargs = new LuisUtteranceResultEventArgs();
args.Result = result;
args.Status = "Succeeded";
args.Message = $"Top intent is {result.TopScoringIntent.Name} with score {result.TopScoringIntent.Score}. Found {result.Entities.Count} entities.";
RaiseOnLuisUtteranceResultUpdated(args);
}
在此基础上,我们转向我们的视图模型。打开LuisViewModel.cs文件。确保类是public的,并且继承自ObservableObject类。
声明一个私有成员,如下所示:
private Luis _luis;
这将保存我们之前创建的Luis对象:
public LuisViewModel() {
_luis = new Luis(new LuisClient("APP_ID_HERE", "API_KEY_HERE"));
我们的构造函数创建Luis对象,确保它使用一个新的LuisClient初始化。如您所注意到的,这需要两个参数,应用程序 ID 和订阅 ID。还有一个第三个参数,preview,但在此我们不需要设置它。
应用程序 ID 可以通过查看发布步骤中的 URL 或访问应用程序网站上的 设置 来找到 www.luis.ai。在那里,您将找到 应用程序 ID,如下面的截图所示:
创建了 Luis 对象后,我们按如下方式完成构造函数:
_luis.OnLuisUtteranceResultUpdated += OnLuisUtteranceResultUpdated;
ExecuteUtteranceCommand = new DelegateCommand(ExecuteUtterance, CanExecuteUtterance);
}
这将连接 OnLuisUtteranceResultUpdated 事件并为我们的按钮创建一个新的 DelegateCommand 事件。为了使我们的命令能够运行,我们需要检查我们是否在输入字段中输入了一些文本。这是使用 CanExecuteUtterance 完成的。
ExecuteUtterance 命令本身相当简单,如下面的代码所示:
private async void ExecuteUtterance(object obj) {
await _luis.RequestAsync(InputText);
}
我们所做的一切只是调用 _luis 对象中的 RequestAsync 函数。我们不需要等待任何结果,因为这些结果将来自事件。
事件处理程序 OnLuisUtteranceResultUpdated 将格式化结果并将它们打印到屏幕上。
首先,我们确保在当前调度线程中调用方法。这是在另一个线程中触发事件时完成的。我们创建一个 StringBuilder,它将用于连接所有结果,如下面的代码所示:
private void OnLuisUtteranceResultUpdated(object sender, LuisUtteranceResultEventArgs e) {
Application.Current.Dispatcher.Invoke(() => {
StringBuilder sb = new StringBuilder();
首先,我们添加 Status 和 Message 状态。然后我们检查是否有检测到的实体,并添加实体数量,如下所示:
sb.AppendFormat("Status: {0}\n", e.Status);
sb.AppendFormat("Summary: {0}\n\n", e.Message);
if(e.Result.Entities != null&&e.Result.Entities.Count != 0) {
sb.AppendFormat("Entities found: {0}\n", e.Result.Entities.Count);
sb.Append("Entities:\n");
如果我们有任何实体,我们将遍历每个实体,打印出实体名称和值:
foreach(var entities in e.Result.Entities) {
foreach(var entity in entities.Value) {
sb.AppendFormat("Name: {0}\tValue: {1}\n",
entity.Name, entity.Value);
}
}
sb.Append("\n");
}
最后,我们将 StringBuilder 添加到我们的 ResultText 字符串中,它应该在屏幕上显示,如下所示:
ResultText = sb.ToString();
});
}
一切编译完成后,结果应该看起来像下面的截图:
通过积极使用来改进模型
LUIS 是一个机器学习服务。因此,我们创建的应用程序和生成的模型可以根据使用情况进行改进。在整个开发过程中,关注性能是一个好主意。您可能会注意到一些经常被错误标记的意图,或者难以识别的实体。
可视化性能
在 LUIS 网站上,仪表板显示有关意图和实体分解的信息。这基本上是关于意图和实体如何在已使用的语句中分布的信息。
以下图表显示了意图分解显示的外观:
以下图表显示了实体分解的外观:
通过将鼠标悬停在不同的条形图(或饼图的扇区)上,将显示意图/实体的名称。此外,还会显示使用中意图/实体的总数百分比。
解决性能问题
如果您在应用程序中注意到错误,通常有四种选项可以解决它:
-
添加模型功能
-
添加标记的语句
-
寻找错误的语句标签
-
修改模式
我们现在将简要地看一下这些中的每一个。
添加模型功能
添加模型功能通常是我们可以做到的,如果我们有应该被检测为实体的短语,但还没有。我们已经看到了一个例子,那就是房间实体,其中一个房间可能是客厅。
解决方案当然是添加短语列表或正则表达式功能。有三种情况这可能会有所帮助:
-
当 LUIS 无法识别相似的字词或短语时。
-
当 LUIS 在识别实体时遇到困难时,将短语列表中所有可能的实体值添加进去应该会有所帮助。
-
当使用稀有或专有词汇时。
添加标记的语句
添加和标记更多语句将始终提高性能。这很可能有助于以下场景:
-
当 LUIS 无法区分两个意图时
-
当 LUIS 无法检测到周围词汇之间的实体时
-
如果 LUIS 系统性地给一个意图分配了很低的分数
寻找错误的语句标签
一个常见的错误是错误地标记语句或实体。在这种情况下,您需要找到错误的语句并更正它。这可能会解决以下场景中的问题:
-
如果 LUIS 无法区分两个意图,即使相似的语句已经被标记
-
如果 LUIS 持续地错过一个实体
修改模式
如果所有前面的解决方案都失败了,您仍然有模型问题,您可能需要考虑更改模式,这意味着合并、重新分组和/或删除意图和实体。
请记住,如果对人类来说标记语句很困难,那么对机器来说就更加困难了。
主动学习
LUIS 的一个非常不错的功能是主动学习的力量。当我们积极使用这项服务时,它会记录所有查询,因此我们可以分析使用情况。这样做可以让我们快速纠正错误,并标记我们之前没有见过的语句。
使用我们构建的应用程序——智能家庭应用程序——如果我们用语句你能告诉我卧室的温度吗?进行查询,模型可能不会识别这一点。如果我们调试这个过程,逐步通过ProcessResult函数,我们将看到以下返回值:
如您从前面的截图中所见,得分最高的意图是None,得分为0.61。此外,没有识别出任何实体,所以这并不好。
返回 LUIS 网站。转到审查端点语句页面,该页面可以在左侧菜单中找到。在这里,我们可以看到我们刚刚尝试的语句已经被添加。现在我们可以正确地标记意图和实体,如下面的截图所示:
通过将话语正确地标记为意图和实体,我们将在下次以这种方式查询时得到正确的结果,如下面的截图所示:
摘要
在本章中,我们创建了一个 LUIS 应用程序。你学习了如何创建语言理解模型,这些模型可以识别句子中的实体。你学习了如何理解用户的意图以及我们如何从这个意图中触发动作。一个重要的步骤是了解如何以各种方式改进模型。
在下一章中,我们将利用在这里学到的知识,使用 LUIS 和语音 API,使我们能够与应用程序进行语音交互。
第五章. 与应用程序对话
在上一章中,我们学习了如何根据话语发现和理解用户的意图。在本章中,我们将学习如何为我们的应用程序添加音频功能,将文本转换为语音和语音转换为文本,以及如何识别说话者。在本章中,我们将学习如何利用语音音频来验证一个人。最后,我们将简要介绍如何自定义语音识别,使其适用于您的应用程序的使用。
到本章结束时,我们将涵盖以下主题:
-
将语音音频转换为文本和文本转换为语音音频
-
通过利用 LUIS 识别语音音频中的意图
-
验证说话者是否为其声称的身份
-
识别说话者
-
定制说话者识别 API 以识别自定义说话风格和环境
文本到音频和音频到文本的转换
在第一章 使用 Microsoft 认知服务入门 中,我们使用了 Bing Speech API 的一部分。我们给了示例应用程序说句子的能力。现在我们将使用在那个示例中创建的代码,但我们将更深入地探讨细节。
我们还将介绍 Bing Speech API 的另一个功能,即将语音音频转换为文本。想法是我们可以对智能屋应用程序说话,该应用程序将识别我们在说什么。使用文本输出,应用程序将使用 LUIS 来收集我们句子的意图。如果 LUIS 需要更多信息,应用程序将通过音频礼貌地要求我们提供更多信息。
要开始,我们希望修改智能屋应用程序的构建定义。我们需要指定我们是在 32 位还是 64 位操作系统上运行它。为了利用语音到文本转换,我们希望安装 Bing Speech NuGet 客户端包。搜索 Microsoft.ProjectOxford.SpeechRecognition 并根据您的系统安装 32 位版本或 64 位版本。
进一步,我们需要添加对 System.Runtime.Serialization 和 System.Web 的引用。这些引用是必要的,以便我们能够进行网络请求并反序列化来自 API 的响应数据。
与应用程序对话
在 Model 文件夹中添加一个新文件,命名为 SpeechToText.cs。在自动创建的 SpeechToText 类下面,我们希望添加一个名为 SttStatus 的 enum 类型变量。它应该有两个值,Success 和 Error。
此外,我们希望定义一个用于我们在执行期间将引发的事件的 EventArgs 类。在文件底部添加以下类:
public class SpeechToTextEventArgs : EventArgs
{
public SttStatus Status { get; private set; }
public string Message { get; private set; }
public List<string> Results { get; private set; }
public SpeechToTextEventArgs(SttStatus status,
string message, List<string> results = null)
{
Status = status;
Message = message;
Results = results;
}
}
如您所见,event 参数将包含操作状态、任何类型的消息以及一个字符串列表。这将是一个包含潜在语音到文本转换的列表。
SpeechToText 类需要实现 IDisposable。这样做是为了我们可以清理用于记录语音音频的资源并正确关闭应用程序。我们将在稍后添加详细信息,所以现在只需确保添加 Dispose 函数。
现在,我们需要在类中定义一些私有成员,以及一个事件:
public event EventHandler<SpeechToTextEventArgs> OnSttStatusUpdated;
private DataRecognitionClient _dataRecClient;
private MicrophoneRecognitionClient _micRecClient;
private SpeechRecognitionMode _speechMode = SpeechRecognitionMode.ShortPhrase;
private string _language = "en-US";
private bool _isMicRecording = false;
当我们有新的操作状态时,OnSttStatusUpdated 事件将被触发。DataRecognitionClient 和 MicrophoneRecognitionClient 是我们可以用来调用 Bing 语音 API 的两个对象。我们将现在看看它们是如何创建的。
我们将 SpeechRecognitionMode 定义为 ShortPhrase。这意味着我们不会期望任何超过 15 秒的语音句子。另一种选择是 LongDictation,这意味着我们可以将语音句子转换成长达 2 分钟的长度。
最后,我们指定语言为英语,并定义一个 bool 类型的变量,该变量表示我们是否正在记录任何内容。
在我们的构造函数中,我们接受 Bing 语音 API 密钥作为参数。我们将在创建我们的 API 客户端时使用它:
public SpeechToText(string bingApiKey)
{
_dataRecClient = SpeechRecognitionServiceFactory.CreateDataClientWithIntentUsingEndpointUrl(_language, bingApiKey, "LUIS_ROOT_URI");
_micRecClient = SpeechRecognitionServiceFactory.CreateMicrophoneClient(_speechMode, _language, bingApiKey);
Initialize();
}
如您所见,我们通过调用 SpeechRecognitionServiceFactory 创建了 _dataRecClient 和 _micRecClient。对于第一个客户端,我们声明我们想要使用意图识别。所需的参数包括语言、Bing API 密钥、LUIS 应用 ID 和 LUIS API 密钥。通过使用 DataRecognitionClient 对象,我们可以上传带有语音的音频文件。
通过使用 MicrophoneRecognitionClient,我们可以使用麦克风进行实时转换。为此,我们不想进行意图检测,因此调用 CreateMicrophoneClient。在这种情况下,我们只需要指定语音模式、语言和 Bing 语音 API 密钥。
在离开构造函数之前,我们调用 Initialize 函数。在这个函数中,我们为每个客户端订阅某些事件:
private void Initialize()
{
_micRecClient.OnMicrophoneStatus += OnMicrophoneStatus;
_micRecClient.OnPartialResponseReceived += OnPartialResponseReceived;
_micRecClient.OnResponseReceived += OnResponseReceived;
_micRecClient.OnConversationError += OnConversationErrorReceived;
_dataRecClient.OnIntent += OnIntentReceived;
_dataRecClient.OnPartialResponseReceived +=
OnPartialResponseReceived;
_dataRecClient.OnConversationError += OnConversationErrorReceived;
_dataRecClient.OnResponseReceived += OnResponseReceived;
}
如您所见,这两个客户端之间有很多相似之处。两个不同之处在于 _dataRecClient 将通过 OnIntent 事件获取意图,而 _micRecClient 将通过 OnMicrophoneStatus 事件获取麦克风状态。
我们并不真正关心部分响应。然而,在某些情况下,它们可能很有用,因为它们将连续给出当前完成的转换:
private void OnPartialResponseReceived(object sender, PartialSpeechResponseEventArgs e)
{
Debug.WriteLine($"Partial response received:{e.PartialResult}");
}
对于我们的应用,我们将选择将其输出到调试控制台窗口。在这种情况下,PartialResult 是一个包含部分转换文本的字符串:
private void OnMicrophoneStatus(object sender, MicrophoneEventArgs e)
{
Debug.WriteLine($"Microphone status changed to recording: {e.Recording}");
}
我们也不关心当前的麦克风状态。同样,我们将状态输出到调试控制台窗口。
在继续之前,添加一个名为 RaiseSttStatusUpdated 的辅助函数。当被调用时,它应该引发 OnSttStatusUpdated。
当我们调用 _dataRecClient 时,我们可能会从 LUIS 识别意图。在这些情况下,我们想要引发一个事件,其中输出识别到的意图。这是通过以下代码完成的:
private void OnIntentReceived(object sender, SpeechIntentEventArgs e)
{
SpeechToTextEventArgs args = new SpeechToTextEventArgs(SttStatus.Success, $"Intent received: {e.Intent.ToString()}.\n Payload: {e.Payload}");
RaiseSttStatusUpdated(args);
}
我们选择打印出意图信息和Payload。这是一个包含从 LUIS 触发的识别实体、意图和动作的字符串。
如果转换过程中发生任何错误,我们将想要做几件事情。首先,我们想要停止可能正在运行的任何麦克风录音。如果当前操作失败,尝试转换更多内容实际上是没有意义的:
private void OnConversationErrorReceived(object sender, SpeechErrorEventArgs e)
{
if (_isMicRecording) StopMicRecording();
我们将立即创建StopMicRecording。
此外,我们想要通知任何订阅者转换失败。在这种情况下,我们想要提供有关错误代码和错误消息的详细信息:
string message = $"Speech to text failed with status code:{e.SpeechErrorCode.ToString()}, and error message: {e.SpeechErrorText}";
SpeechToTextEventArgs args = new SpeechToTextEventArgs(SttStatus.Error, message);
RaiseSttStatusUpdated(args);
}
幸运的是,OnConversationError事件为我们提供了关于任何错误的详细信息。
现在,让我们看看StopMicRecording方法:
private void StopMicRecording()
{
_micRecClient.EndMicAndRecognition();
_isMicRecording = false;
}
这是一个简单的函数,它会在_micRecClient MicrophoneRecognitionClient对象上调用EndMicAndRecognition。当这个函数被调用时,我们将停止客户端的录音。
我们需要创建的最后一个事件处理程序是OnResponseReceived处理程序。这将在我们从服务接收到完整、转换后的响应时被触发。
再次强调,如果我们正在录音,我们想要确保不再记录任何内容:
private void OnResponseReceived(object sender, SpeechResponseEventArgs e)
{
if (_isMicRecording) StopMicRecording();
SpeechResponseEventArgs参数包含一个PhraseResponse对象。它包含一个RecognizedPhrase数组,我们想要访问。数组中的每个项目都包含正确转换的置信度。它还包含作为DisplayText的转换后的短语。这使用逆文本归一化、正确的首字母大小写和标点符号,并用星号屏蔽亵渎性词汇:
RecognizedPhrase[] recognizedPhrases = e.PhraseResponse.Results;
List<string> phrasesToDisplay = new List<string>();
foreach(RecognizedPhrase phrase in recognizedPhrases)
{
phrasesToDisplay.Add(phrase.DisplayText);
}
我们可能还会以以下表格中描述的其他格式获得转换后的短语:
| 格式 | 描述 |
|---|---|
LexicalForm | 这是原始的、未经处理的识别结果。 |
InverseTextNormalizationResult | 这将短语如one two three four显示为1234,因此它非常适合像go to second street这样的用途。 |
MaskedInverseTextNormalizationResult | 逆文本归一化和亵渎性屏蔽。不应用首字母大小写或标点符号。 |
对于我们的使用,我们只对DisplayText感兴趣。有了识别短语列表,我们将引发状态更新事件:
SpeechToTextEventArgs args = new SpeechToTextEventArgs(SttStatus.Success, $"STT completed with status: {e.PhraseResponse.RecognitionStatus.ToString()}", phrasesToDisplay);
RaiseSttStatusUpdated(args);
}
为了能够使用这个类,我们需要几个公共函数,这样我们就可以开始语音识别:
public void StartMicToText()
{
_micRecClient.StartMicAndRecognition();
_isMicRecording = true;
}
StartMicToText方法将在_micRecClient对象上调用StartMicAndRecognition方法。这将允许我们使用麦克风将语音音频转换。这个函数将成为我们访问此 API 的主要方式:
public void StartAudioFileToText(string audioFileName) {
using (FileStream fileStream = new FileStream(audioFileName, FileMode.Open, FileAccess.Read))
{
int bytesRead = 0;
byte[] buffer = new byte[1024];
第二个函数将需要一个音频文件的文件名,这是我们想要转换的音频。我们以读取权限打开文件,并准备好读取它:
try {
do {
bytesRead = fileStream.Read(buffer, 0, buffer.Length);
_dataRecClient.SendAudio(buffer, bytesRead);
} while (bytesRead > 0);
}
只要我们有可用数据,我们就从文件中读取。我们将填充buffer,并调用SendAudio方法。这将触发服务中的识别操作。
如果发生任何异常,我们确保将异常消息输出到调试窗口。最后,我们需要调用EndAudio方法,以便服务不等待任何更多数据:
catch(Exception ex) {
Debug.WriteLine($"Exception caught: {ex.Message}");
}
finally {
_dataRecClient.EndAudio();
}
在离开这个班级之前,我们需要处理我们的 API 客户端。在Dispose函数中添加以下内容:
if (_micRecClient != null) {
_micRecClient.EndMicAndRecognition();
_micRecClient.OnMicrophoneStatus -= OnMicrophoneStatus;
_micRecClient.OnPartialResponseReceived -= OnPartialResponseReceived;
_micRecClient.OnResponseReceived -= OnResponseReceived;
_micRecClient.OnConversationError -= OnConversationErrorReceived;
_micRecClient.Dispose();
_micRecClient = null;
}
if(_dataRecClient != null) {
_dataRecClient.OnIntent -= OnIntentReceived;
_dataRecClient.OnPartialResponseReceived -= OnPartialResponseReceived;
_dataRecClient.OnConversationError -= OnConversationErrorReceived;
_dataRecClient.OnResponseReceived -= OnResponseReceived;
_dataRecClient.Dispose();
_dataRecClient = null;
}
我们停止麦克风录音,取消订阅所有事件,并销毁和清除客户端对象。
确保在继续之前应用程序已经编译。我们现在将探讨如何使用这个类。
允许应用程序进行语音反馈
我们已经看到了如何使应用程序对我们说话。我们将使用我们在第一章中创建的相同类,使用 Microsoft 认知服务入门。从第一章的示例项目中复制Authentication.cs和TextToSpeech.cs到Model文件夹中。确保相应地更改命名空间。
由于我们已经通过了代码,我们不会再次进行审查。相反,我们将查看第一章中省略的一些细节,使用 Microsoft 认知服务入门。
音频输出格式
音频输出格式可以是以下格式之一:
-
raw-8khz-8bit-mono-mulaw -
raw-16khz-16bit-mono-pcm -
riff-8khz-8bit-mono-mulaw -
riff-16khz-16bit-mono-pcm
错误代码
在调用 API 时可能会出现四种可能的错误代码。这些代码在以下表格中描述:
| 代码 | 描述 |
|---|---|
400 / BadRequest | 缺少必需的参数,为空或为 null。或者,参数无效。一个例子可能是一个超过允许长度的字符串。 |
401 / Unauthorized | 请求未授权。 |
413 / RequestEntityTooLarge | SSML 输入大于支持的大小。 |
502 / BadGateway | 与网络或服务器相关的问题。 |
支持的语言
支持以下语言:
英语(澳大利亚),英语(英国),英语(美国),英语(加拿大),英语(印度),西班牙语,墨西哥西班牙语,德语,阿拉伯语(埃及),法语,加拿大法语,意大利语,日语,葡萄牙语,俄语,中文(简体),中文(香港),中文(繁体)。
基于语音命令利用 LUIS
为了利用我们刚刚添加的功能,我们将修改LuisView和LuisViewModel。在视图中添加一个新的Button,这将确保我们记录命令。在视图模型中添加相应的ICommand。
我们还需要向类中添加一些其他成员:
private SpeechToText _sttClient;
private TextToSpeech _ttsClient;
private string _bingApiKey = "BING_SPEECH_API_KEY";
前两个将用于将语音音频和文本之间进行转换。第三个是 Bing 语音 API 的 API 密钥。
让视图模型实现IDisposable,并显式销毁SpeechToText对象。
通过在构造函数中添加以下内容来创建对象:
_sttClient = new SpeechToText(_bingApiKey);
_sttClient.OnSttStatusUpdated += OnSttStatusUpdated;
_ttsClient = new TextToSpeech();
_ttsClient.OnAudioAvailable += OnTtsAudioAvailable;
_ttsClient.OnError += OnTtsError;
GenerateHeaders();
这将创建客户端对象并订阅所需的事件。最后,它将调用一个函数来生成用于 REST API 调用的身份验证令牌。此函数应如下所示:
private async void GenerateHeaders()
{
if (await _ttsClient.GenerateAuthenticationToken(_bingApiKey))
_ttsClient.GenerateHeaders();
}
如果我们从 _ttsClient 收到任何错误,我们希望将其输出到调试控制台:
private void OnTtsError(object sender, AudioErrorEventArgs e)
{
Debug.WriteLine($"Status: Audio service failed - {e.ErrorMessage}");
}
我们不需要将此输出到 UI,因为这只是一个可选功能。
如果我们有音频可用,我们想确保播放它。我们通过创建一个 SoundPlayer 对象来实现这一点:
private void OnTtsAudioAvailable(object sender, AudioEventArgs e)
{
SoundPlayer player = new SoundPlayer(e.EventData);
player.Play();
e.EventData.Dispose();
}
使用从事件参数中获得的音频流,我们可以播放音频给用户。
如果我们收到 _sttClient 的状态更新,我们想在文本框中显示此信息。
如果我们成功识别了语音音频,我们希望显示可用的 Message 字符串:
private void OnSttStatusUpdated(object sender, SpeechToTextEventArgs e) {
Application.Current.Dispatcher.Invoke(() => {
StringBuilder sb = new StringBuilder();
if(e.Status == SttStatus.Success) {
if(!string.IsNullOrEmpty(e.Message)) {
sb.AppendFormat("Result message: {0}\n\n", e.Message);
}
我们还希望显示所有识别出的短语。使用第一个可用的短语,我们调用 LUIS:
if(e.Results != null && e.Results.Count != 0) {
sb.Append("Retrieved the following results:\n");
foreach(string sentence in e.Results) {
sb.AppendFormat("{0}\n\n", sentence);
}
sb.Append("Calling LUIS with the top result\n");
CallLuis(e.Results.FirstOrDefault());
}
}
如果识别失败,我们打印出可能有的任何错误消息。最后,我们确保 ResultText 被更新为新的数据:
else {
sb.AppendFormat("Could not convert speech to text:{0}\n", e.Message);
}
sb.Append("\n");
ResultText = sb.ToString();
});
}
新创建的 ICommand 需要一个启动识别过程的功能:
private void RecordUtterance(object obj) {
_sttClient.StartMicToText();
}
函数开始麦克风录音。
最后,我们需要对 OnLuisUtteranceResultUpdated 进行一些修改。进行以下修改,其中我们输出任何 DialogResponse:
if (e.RequiresReply && !string.IsNullOrEmpty(e.DialogResponse))
{
await _ttsClient.SpeakAsync(e.DialogResponse, CancellationToken.None);
sb.AppendFormat("Response: {0}\n", e.DialogResponse);
sb.Append("Reply in the left textfield");
RecordUtterance(sender);
}
else
{
await _ttsClient.SpeakAsync($"Summary: {e.Message}", CancellationToken.None);
}
如果存在 DialogResponse,这将播放它。如果需要,应用程序将要求您提供更多信息。然后它将开始录音,这样我们就可以在不点击任何按钮的情况下回答。
如果不存在 DialogResponse,我们只需让应用程序向我们说出摘要。这将包含来自 LUIS 的意图、实体和动作数据。
知道谁在说话
使用 说话人识别 API,我们可以识别正在说话的人。通过定义一个或多个带有相应样本的说话人配置文件,我们可以识别在任何时候是否有任何人在说话。
为了能够利用此功能,我们需要进行几个步骤:
-
我们需要向服务添加一个或多个说话人配置文件。
-
每个说话人配置文件注册了几个语音样本。
-
我们调用服务以根据音频输入识别说话人。
如果您还没有这样做,请在 portal.azure.com 为说话人识别 API 注册一个 API 密钥。
首先,向您的智能屋应用程序添加一个新的 NuGet 包。搜索并添加 Microsoft.ProjectOxford.SpeakerRecognition。
在您的项目 Model 文件夹中添加一个名为 SpeakerIdentification 的新类。此类将包含与说话人识别相关的所有功能。
在类别下方,我们将添加另一个类别,包含用于状态更新的 EventArgs:
public class SpeakerIdentificationStatusUpdateEventArgs : EventArgs
{
public string Status { get; private set; }
public string Message { get; private set; }
public Identification IdentifiedProfile { get; set; }
public SpeakerIdentificationStatusUpdateEventArgs (string status, string message)
{
Status = status;
Message = message;
}
}
前两个属性应该是显而易见的。最后一个,IdentificationProfile,将包含成功识别过程的结果。我们将现在查看它包含哪些信息。
我们还希望发送错误事件,因此让我们添加一个EventArgs类来存储所需的信息:
public class SpeakerIdentificationErrorEventArgs : EventArgs {
public string ErrorMessage { get; private set; }
public SpeakerIdentificationErrorEventArgs(string errorMessage)
{
ErrorMessage = errorMessage;
}
}
再次强调,属性应该是自解释的。
在SpeakerIdentification类中,在类顶部添加两个事件和一个私有成员:
public event EventHandler <SpeakerIdentificationStatusUpdateEventArgs>
OnSpeakerIdentificationStatusUpdated;
public event EventHandler <SpeakerIdentificationErrorEventArgs>
OnSpeakerIdentificationError;
private ISpeakerIdentificationServiceClient _speakerIdentificationClient;
如果我们有任何状态更新、成功识别或错误,事件将被触发。ISpeakerIdentificationServiceClient对象是访问演讲者识别 API 的入口点。通过构造函数注入此对象。
为了更容易触发事件,添加两个辅助函数,每个事件一个。将这些函数命名为RaiseOnIdentificationStatusUpdated和RaiseOnIdentificationError。它们应该接受相应的EventArgs对象作为参数并触发相应的事件。
添加演讲者配置文件
为了能够识别演讲者,我们需要添加配置文件。每个配置文件可以看作是一个独特的人,我们可以在以后识别。
在撰写本文时,每个订阅允许创建 1,000 个演讲者配置文件。这还包括为验证创建的配置文件,我们将在下面查看。
为了便于创建配置文件,我们需要向我们的AdministrationView和AdministrationViewModel属性中添加一些元素,因此请打开这些文件。
在视图中,添加一个用于添加演讲者配置文件的新按钮。还要添加一个列表框,它将显示所有我们的配置文件。如何布局 UI 取决于你。
ViewModel 需要一个新ICommand属性用于按钮。它还需要一个ObservableObject属性用于我们的配置文件列表;确保它是Guid类型。我们还需要能够选择一个配置文件,因此添加一个用于所选配置文件的Guid属性。
此外,我们还需要向 ViewModel 添加一个新成员:
private SpeakerIdentification _speakerIdentification;
这是我们之前创建的类的引用。在构造函数中创建此对象,传递一个ISpeakerIdentificationServiceClient对象,该对象通过 ViewModel 的构造函数注入。在构造函数中,你还应该订阅我们创建的事件:
_speakerIdentification.OnSpeakerIdentificationError += OnSpeakerIdentificationError;
_speakerIdentification.OnSpeakerIdentificationStatusUpdated += OnSpeakerIdentificationStatusUpdated;
基本上,我们希望两个事件处理程序都更新状态文本,以包含它们携带的消息:
Application.Current.Dispatcher.Invoke(() =>
{
StatusText = e.Message;
});
上述代码是用于OnSpeakerIdentificationStatusUpdated的。对于OnSpeakerIdentificationError也应使用相同的代码,但将StatusText设置为e.ErrorMessage。
在为我们的ICommand属性创建的函数中,我们执行以下操作以创建一个新的配置文件:
private async void AddSpeaker(object obj)
{
Guid speakerId = await _speakerIdentification.CreateSpeakerProfile();
我们调用我们的_speakerIdentification对象的CreateSpeakerProfile函数。此函数将返回一个Guid,这是该演讲者的唯一 ID。在我们的示例中,我们不做任何进一步的操作。在实际应用中,我建议以某种方式将此 ID 映射到名称。正如你将看到的,通过 GUID 识别人是为机器,而不是人:
GetSpeakerProfiles();
}
我们通过调用我们将在下面创建的GetSpeakerProfile函数来完成此函数。这将获取我们创建的所有配置文件的列表,以便我们可以在后续过程中使用这些信息:
private async void GetSpeakerProfiles()
{
List<Guid> profiles = await _speakerIdentification.ListSpeakerProfiles();
if (profiles == null) return;
在我们的 GetSpeakerProfiles 函数中,我们在 _speakerIdentification 对象上调用 ListSpeakerProfiles。这将,正如我们目前所看到的,获取一个包含资料 ID 的 GUID 列表。如果这个列表为空,就没有继续的必要:
foreach(Guid profile in profiles)
{
SpeakerProfiles.Add(profile);
}
}
如果列表中包含任何内容,我们将这些 ID 添加到我们的 SpeakerProfiles 中,这是一个 ObservableCollection 属性。这将显示我们所有的资料在 UI 中。
此函数也应从 Initialize 函数中调用,因此我们在启动应用程序时填充列表。
在 SpeakerIdentification 类中,创建一个名为 CreateSpeakerProfile 的新函数。这个函数应该有 Task<Guid> 返回类型,并标记为 async:
public async Task<Guid> CreateSpeakerProfile()
{
try
{
CreateProfileResponse response = await _speakerIdentificationClient.CreateProfileAsync("en-US");
然后,我们将对 API 对象调用 CreateProfileAsync。我们需要指定用于演讲者资料的区域设置。在编写本文时,en-US 是唯一有效的选项。
如果调用成功,我们将收到一个 CreateProfileResponse 对象作为响应。这个对象包含新创建的演讲者资料的 ID:
if (response == null)
{
RaiseOnIdentificationError(
new SpeakerIdentificationErrorEventArgs
("Failed to create speaker profile."));
return Guid.Empty;
}
return response.ProfileId;
}
如果 response 为空,我们引发一个错误事件。如果它包含数据,我们将返回 ProfileId 给调用者。
添加相应的 catch 子句以完成函数。
创建一个名为 ListSpeakerProfile 的新函数。这个函数应该返回 Task<List<Guid>> 并标记为 async:
public async Task<List<Guid>> ListSpeakerProfiles()
{
try
{
List<Guid> speakerProfiles = new List<Guid>();
Profile[] profiles = await _speakerIdentificationClient.GetProfilesAsync();
然后,我们将创建一个类型为 Guid 的列表,这是我们将要返回的演讲者资料列表。然后,我们在 _speakerIdentificationClient 对象上调用 GetProfilesAsync 方法。这将给我们一个类型为 Profile 的数组,其中包含每个资料的信息。这些信息包括创建时间、注册状态、最后修改等。我们感兴趣的是每个资料的 ID:
if (profiles == null || profiles.Length == 0)
{
RaiseOnIdentificationError(new SpeakerIdentificationErrorEventArgs("No profiles exist"));
return null;
}
foreach (Profile profile in profiles)
{
speakerProfiles.Add(profile.ProfileId);
}
return speakerProfiles;
}
如果返回任何资料,我们将遍历数组,并将每个 profileId 添加到之前创建的列表中。然后,将此列表返回给调用者,在我们的例子中将是 ViewModel。
使用相应的 catch 子句结束函数。在继续之前,确保代码能够编译并按预期执行。这意味着你现在应该能够将演讲者资料添加到服务中,并在 UI 中显示创建的资料。
要删除演讲者资料,我们需要在 SpeakerIdentification 中添加一个新函数。将此函数命名为 DeleteSpeakerProfile,并让它接受一个 Guid 作为其参数。这将是我们想要删除的给定资料的 ID。将函数标记为 async。函数看起来应该如下所示:
public async void DeleteSpeakerProfile(Guid profileId)
{
try
{
await _speakerIdentificationClient.DeleteProfileAsync(profileId);
}
catch (IdentificationException ex)
{
RaiseOnIdentificationError(new SpeakerIdentificationErrorEventArgs($"Failed to
delete speaker profile: {ex.Message}"));
}
catch (Exception ex)
{
RaiseOnIdentificationError(new SpeakerIdentificationErrorEventArgs($"Failed to
delete speaker profile: {ex.Message}"));
}
}
如您所见,调用 DeleteProfileAsync 方法期望一个 Guid 类型的 profileId。没有返回值,因此当我们调用此函数时,我们需要在我们的 ViewModel 中调用 GetSpeakerProfile 方法。
为了便于删除演讲者资料,向 UI 添加一个新按钮,并在 ViewModel 中添加相应的 ICommand 属性。
注册个人资料
在设置好扬声器配置文件后,我们需要将语音音频与配置文件关联起来。我们通过一个称为注册的过程来完成这个任务。对于扬声器识别,注册是文本无关的。这意味着你可以使用任何句子进行注册。一旦录音完成,将提取一系列特征来形成一个独特的声纹。
在注册时,你使用的音频文件必须至少 5 秒,最多 5 分钟。最佳实践建议你至少积累 30 秒的语音。这是在移除静音后的 30 秒,因此可能需要多个音频文件。我们可以通过指定一个额外的参数来避免这个建议,正如我们接下来将要看到的。
你选择如何上传音频文件取决于你。在智能家居应用程序中,我们将使用麦克风来录制实时音频。为此,我们需要添加一个新的 NuGet 包,名为NAudio。这是一个.NET 的音频库,它简化了音频工作。
我们还需要一个处理录音的类,这超出了本书的范围。因此,我建议你复制Recording.cs文件,该文件可以在示例项目的Model文件夹中找到。
在AdministrationViewModel ViewModel 中,添加一个新复制的类的私有成员。创建这个类并订阅在Initialize函数中定义的事件:
_recorder = new Recording();
_recorder.OnAudioStreamAvailable += OnRecordingAudioStreamAvailable;
_recorder.OnRecordingError += OnRecordingError;
我们有一个错误事件和一个可用音频流事件。让OnRecordingError将ErrorMessage打印到状态文本字段。
在OnAudioStreamAvailable中添加以下内容:
Application.Current.Dispatcher.Invoke(() =>
{
_speakerIdentification.CreateSpeakerEnrollment(e.AudioStream, SelectedSpeakerProfile);
});
在这里,我们在_speakerIdentification对象上调用CreateSpeakerEnrollment。我们将在后面介绍这个函数。我们传递的参数包括从录音中获取的AudioStream以及所选配置文件的 ID。
为了能够获取用于注册的音频文件,我们需要开始和停止录音。这可以通过简单地添加两个新按钮来完成,一个用于开始,一个用于停止。然后它们需要执行以下操作:
_recorder.StartRecording();
_recorder.StopRecording();
在SpeakerIdentification.cs文件中,我们需要创建一个新的函数CreateSpeakerEnrollment。这个函数应该接受Stream和Guid作为参数,并标记为async:
public async void CreateSpeakerEnrollment(Stream audioStream, Guid profileId) {
try {
OperationLocation location = await _speakerIdentificationClient.EnrollAsync(audioStream, profileId);
在这个函数中,我们在_speakerIdentificationClient上调用EnrollAsync函数。这个函数需要audioStream和profileId作为参数。一个可选的第三个参数是一个bool类型的变量,它让你决定是否使用推荐的语音长度。默认值是false,这意味着你使用至少 30 秒的语音推荐设置。
如果调用成功,我们将返回一个OperationLocation对象。这个对象包含一个 URL,我们可以通过它查询注册状态,这正是我们接下来要做的:
if (location == null) {
RaiseOnIdentificationError(new SpeakerIdentificationErrorEventArgs("Failed to start enrollment process."));
return;
}
GetEnrollmentOperationStatus(location);
}
首先,我们确保我们有了 location 数据。如果没有它,就没有继续下去的意义。如果我们确实有 location 数据,我们调用一个函数,GetEnrollmentOperationStatus,指定 location 作为参数。
为完成函数,添加相应的 catch 子句。
GetEnrollmentOperationStatus 方法接受 OperationLocation 作为参数。当我们进入函数时,我们进入一个 while 循环,该循环将一直运行,直到操作完成。我们调用 CheckEnrollmentStatusAsync,指定 location 作为参数。如果这个调用成功,它将返回一个 EnrollmentOperation 对象,其中包含状态、注册语音时间和剩余注册时间的估计:
private async void GetEnrollmentOperationStatus(OperationLocation location) {
try {
while(true) {
EnrollmentOperation result = await _speakerIdentificationClient.CheckEnrollmentStatusAsync(location);
当我们检索到结果后,我们检查状态是否正在运行。如果不是,操作可能已失败、成功或尚未开始。在任何情况下,我们都不想进一步检查,因此我们发送一个包含状态的更新,并跳出循环:
if(result.Status != Status.Running)
{
RaiseOnIdentificationStatusUpdated(new SpeakerIdentificationStatusUpdateEventArgs(result.Status.ToString(),
$"Enrollment finished. Enrollment status: {result.ProcessingResult.EnrollmentStatus.ToString()}"));
break;
}
RaiseOnIdentificationStatusUpdated(new SpeakerIdentificationStatusUpdateEventArgs(result.Status.ToString(), "Enrolling..."));
await Task.Delay(1000);
}
}
如果状态仍然是正在运行,我们更新状态,并在再次尝试之前等待 1 秒。
注册完成后,可能会有需要重置给定个人资料注册的情况。我们可以在 SpeakerIdentification 中创建一个新函数。命名为 ResetEnrollments,并让它接受一个 Guid 作为参数。这应该是要重置的说话者个人资料的 ID。在 try 子句中执行以下操作:
await _speakerIdentificationClient .ResetEnrollmentsAsync(profileId);
这将删除与给定个人资料关联的所有音频文件,并重置注册状态。要调用此函数,请向 UI 添加一个新按钮,并在 ViewModel 中添加相应的 ICommand 属性。
如果您编译并运行应用程序,您可能会得到以下截图类似的结果:
识别说话者
最后一步是识别说话者,我们将在 HomeView 和相应的 HomeViewModel 中完成这项工作。我们不需要修改 UI 很多,但我们需要添加两个按钮来开始和停止录音。或者,如果您不使用麦克风,您可以使用一个按钮来浏览音频文件。无论如何,在 ViewModel 中添加相应的 ICommand 属性。
我们还需要为 Recording 和 SpeakerIdentification 类添加私有成员。这两个成员都应该在构造函数中创建,在那里我们应该注入 ISpeakerIdentificationServiceClient。
在 Initialize 函数中,订阅所需的事件:
_speakerIdentification.OnSpeakerIdentificationError += OnSpeakerIdentificationError;
_speakerIdentification.OnSpeakerIdentificationStatusUpdated += OnSpeakerIdentificationStatusReceived;
_recording.OnAudioStreamAvailable += OnSpeakerRecordingAvailable;
_recording.OnRecordingError += OnSpeakerRecordingError;
对于两个错误事件处理器,OnSpeakerRecordingError 和 OnSpeakerIdentificationError,我们不想在这里打印错误信息。为了简单起见,我们只需将其输出到调试控制台窗口。
当我们录制了一些音频时,OnSpeakerRecordingAvailable 事件将被触发。这是将触发尝试识别说话者的事件处理器。
我们需要做的第一件事是获取说话者配置文件 ID 的列表。我们通过调用前面提到的ListSpeakerProfiles来实现这一点:
private async void OnSpeakerRecordingAvailable(object sender, RecordingAudioAvailableEventArgs e)
{
try
{
List<Guid> profiles = await _speakerIdentification.ListSpeakerProfiles();
使用说话者配置文件列表,我们在_speakerIdentification对象上调用IdentifySpeaker方法。我们将记录的音频流和配置文件列表作为参数传递给函数:
_speakerIdentification.IdentifySpeaker(e.AudioStream, profiles.ToArray());
}
通过添加相应的catch子句来完成事件处理程序。
在SpeakerIdentification.cs文件中,我们添加新的函数IdentifySpeaker:
public async void IdentifySpeaker(Stream audioStream, Guid[] speakerIds)
{
try
{
OperationLocation location = await _speakerIdentificationClient.IdentifyAsync(audioStream, speakerIds);
函数应标记为async并接受一个Stream和一个Guid数组作为参数。为了识别说话者,我们在_speakerIdentificationClient对象上调用IdentifyAsync函数。这需要一个音频文件,以Stream的形式,以及一个配置文件 ID 数组。可选的第三个参数是一个bool,你可以用它来指示你是否想偏离推荐的语音长度。
如果调用成功,我们将返回一个OperationLocation对象。它包含一个我们可以用来检索当前识别过程状态的 URL:
if (location == null)
{
RaiseOnIdentificationError(new SpeakerIdentificationErrorEventArgs ("Failed to identify speaker."));
return;
}
GetIdentificationOperationStatus(location);
}
如果结果数据中没有内容,我们不想做任何其他事情。如果它包含数据,我们将它作为参数传递给GetIdentificationOperationStatus方法:
private async void GetIdentificationOperationStatus (OperationLocation location)
{
try
{
while (true)
{
IdentificationOperation result = await _speakerIdentificationClient.CheckIdentificationStatusAsync(location);
这个函数与GetEnrollmentOperationStatus非常相似。我们进入一个while循环,它将一直运行,直到操作完成。我们调用CheckIdentificationStatusAsync,传递location作为参数,得到IdentificationOperation作为结果。这将包含数据,如状态、已识别的配置文件 ID 和正确结果的置信度。
如果操作未运行,我们以状态消息和ProcessingResult引发事件。如果操作仍在运行,我们更新状态,并在 1 秒后再次尝试:
if (result.Status != Status.Running)
{
RaiseOnIdentificationStatusUpdated(new SpeakerIdentificationStatusUpdateEventArgs(result.Status.ToString(), $"Enrollment finished with message:{result.Message}.") { IdentifiedProfile = result.ProcessingResult });
break;
}
RaiseOnIdentificationStatusUpdated(new SpeakerIdentificationStatusUpdateEventArgs(result.Status.ToString(), "Identifying..."));
await Task.Delay(1000);
}
}
在返回到HomeViewModel之前,添加相应的catch子句。
最后一个拼图是创建OnSpeakerIdentificationStatusReceived。在HomeViewModel中添加以下代码:
Application.Current.Dispatcher.Invoke(() =>
{
if (e.IdentifiedProfile == null) return;
SystemResponse = $"Hi there,{e.IdentifiedProfile.IdentifiedProfileId}";
});
我们需要检查是否有一个已识别的配置文件。如果没有,我们离开函数。如果有已识别的配置文件,我们向屏幕给出响应,说明是谁。
就像应用的管理端一样,这是一个方便的地方,可以有一个名称到配置文件 ID 的映射。正如以下结果截图所示,在众多GUID中识别一个并不容易:
通过语音验证一个人
验证一个人是否是他们所声称的是一个非常类似的过程。为了展示如何进行,我们将创建一个新的示例项目,因为我们不需要在智能屋应用中实现这个功能。
将 Microsoft.ProjectOxford.SpeakerRecognition 和 NAudio NuGet 包添加到项目中。我们需要之前使用的 Recording 类,因此从智能家居应用的 Model 文件夹中复制此内容。
打开 MainView.xaml 文件。为了示例能够工作,我们需要在 UI 中添加一些元素。添加一个 Button 元素以添加演讲者配置文件。添加两个 Listbox 元素。一个将保存可用的验证短语,而另一个将列出我们的演讲者配置文件。
添加用于删除配置文件、开始和停止注册录音、重置注册以及开始/停止验证录音的 Button 元素。
在 ViewModel 中,你需要添加两个 ObservableCollection 属性:一个类型为 string,另一个类型为 Guid。一个将包含可用的验证短语,而另一个将包含演讲者配置文件列表。你还需要一个属性用于选择演讲者配置文件,我们还想添加一个字符串属性以显示状态。
ViewModel 还需要七个 ICommand 属性,每个按钮一个。
在 Model 文件夹中创建一个新的类,命名为 SpeakerVerification。在此类下创建两个新类,在同一文件中。
第一个是当我们抛出状态更新事件时将传递的事件参数。如果设置,Verification 属性将保存验证结果,我们将在下面看到:
public class SpeakerVerificationStatusUpdateEventArgs : EventArgs
{
public string Status { get; private set; }
public string Message { get; private set; }
public Verification VerifiedProfile { get; set; }
public SpeakerVerificationStatusUpdateEventArgs(string status,string message)
{
Status = status;
Message = message;
}
}
下一个类是一个泛型事件参数,当抛出错误事件时使用。在 SpeakerVerification 本身中,添加以下事件:
public class SpeakerVerificationErrorEventArgs : EventArgs
{
public string ErrorMessage { get; private set; }
public SpeakerVerificationErrorEventArgs(string errorMessage)
{
ErrorMessage = errorMessage;
}
}
为了方便起见,添加辅助函数来引发这些事件。将它们命名为 RaiseOnVerificationStatusUpdated 和 RaiseOnVerificationError。在每个函数中引发正确的事件:
public event EventHandler <SpeakerVerificationStatusUpdateEventArgs> OnSpeakerVerificationStatusUpdated;
public event EventHandler<SpeakerVerificationErrorEventArgs> OnSpeakerVerificationError;
我们还需要添加一个名为 ISpeakerVerificationServiceClient 的私有成员。它将负责调用 API。我们通过构造函数注入它。
向类中添加以下函数:
-
CreateSpeakerProfile:无参数,异步函数,返回类型Task<Guid> -
ListSpeakerProfile:无参数,异步函数,返回类型Task<List<Guid>> -
DeleteSpeakerProfile:必需参数为Guid,异步函数,无返回值 -
ResetEnrollments:必需参数为Guid,异步函数,无返回值
这些函数的内容可以从智能家居应用中的相应函数复制,因为它们完全相同。唯一的区别是,你需要将 API 调用从 _speakerIdentificationClient 更改为 _speakerVerificationClient。此外,引发事件将需要新创建的事件参数。
接下来,我们需要一个函数来列出验证短语。这些是支持用于验证的短语。在注册配置文件时,你必须说出列表中的句子之一。
创建一个名为 GetVerificationPhrase 的函数。让它返回 Task<List<string>>,并标记为 async:
public async Task<List<string>> GetVerificationPhrase()
{
try
{
List<string> phrases = new List<string>();
VerificationPhrase[] results = await _speakerVerificationClient.GetPhrasesAsync("en-US");
我们将调用GetPhrasesAsync,指定我们想要的短语语言。在撰写本文时,英语是唯一的选择。
如果这个调用成功,我们将返回一个包含VerificationPhrases数组的数组。该数组中的每个元素都包含以下短语的字符串:
foreach(VerificationPhrase phrase in results) {
phrases.Add(phrase.Phrase);
}
return phrases;
}
我们遍历数组并将短语添加到我们的列表中,我们将将其返回给调用者。
因此,我们已经创建了一个配置文件,并且我们有可能的验证短语列表。现在,我们需要进行注册。为了注册,服务要求每个说话者至少有三次注册。这意味着您选择一个短语并至少注册三次。
在进行注册时,强烈建议使用您将用于验证的相同录音设备。
创建一个名为CreateSpeakerEnrollment的新函数。这应该需要一个Stream和一个Guid。第一个参数是用于注册的音频。后者是我们注册的配置文件 ID。该函数应标记为async,并且没有返回值:
public async void CreateSpeakerEnrollment(Stream audioStream, Guid profileId) {
try {
Enrollment enrollmentStatus = await _speakerVerificationClient.EnrollAsync(audioStream, profileId);
当我们调用EnrollAsync时,我们传递audioStream和profileId参数。如果调用成功,我们将返回一个Enrollment对象。它包含注册的当前状态,并指定在完成过程之前需要添加的注册次数。
如果enrollmentStatus为 null,我们退出函数并通知任何订阅者。如果我们确实有状态数据,我们将引发事件以通知它有状态更新,并指定当前状态:
if (enrollmentStatus == null) {
RaiseOnVerificationError(new SpeakerVerificationErrorEventArgs("Failed to start enrollment process."));
return;
}
RaiseOnVerificationStatusUpdate(new SpeakerVerificationStatusUpdateEventArgs("Succeeded", $"Enrollment status:{enrollmentStatus.EnrollmentStatus}"));
}
添加相应的catch子句以完成函数。
在这个类中,我们还需要一个用于验证的函数。要验证说话者,您需要发送一个音频文件。此文件必须至少 1 秒长,最多 15 秒长。您需要录制与注册时相同的短语。
调用VerifySpeaker函数,并使其需要Stream和Guid。流是我们将用于验证的音频文件。Guid是我们希望验证的配置文件 ID。该函数应该是async类型且没有返回类型:
public async void VerifySpeaker(Stream audioStream, Guid speakerProfile) {
try {
Verification verification = await _speakerVerificationClient.VerifyAsync(audioStream, speakerProfile);
我们将从_speakerVerificationClient调用VerifyAsync。所需的参数是audioStream和speakerProfile。
成功的 API 调用将导致响应中返回一个Verification对象。这将包含验证结果,以及结果正确性的置信度:
if (verification == null) {
RaiseOnVerificationError(new SpeakerVerificationErrorEventArgs("Failed to verify speaker."));
return;
}
RaiseOnVerificationStatusUpdate(new SpeakerVerificationStatusUpdateEventArgs("Verified", "Verified speaker") { VerifiedProfile = verification });
}
如果我们有验证结果,我们将引发状态更新事件。添加相应的catch子句以完成函数。
在 ViewModel 中,我们需要连接命令和事件处理器。这与说话者识别的方式类似,因此我们不会详细说明代码。
当代码编译并运行时,结果可能看起来类似于以下截图:
在这里,我们可以看到我们已经创建了一个说话人配置文件。我们已完成注册,并准备好验证说话人。
验证说话人配置文件可能会导致以下结果:
如您所见,验证被高度信任地接受。
如果我们尝试使用不同的短语或让其他人尝试作为特定的说话人配置文件进行验证,我们可能会得到以下结果:
在这里,我们可以看到验证已被拒绝。
自定义语音识别
当我们使用语音识别系统时,有几个组件正在协同工作。其中两个更重要的组件是声学模型和语言模型。第一个将音频的短片段标记为声音单元。第二个帮助系统根据给定单词在特定序列中出现的可能性来决定单词。
尽管微软在创建全面的声学模型和语言模型方面做得很好,但仍然可能存在需要自定义这些模型的情况。
想象一下,您有一个应该在工厂环境中使用的应用程序。使用语音识别将需要对该环境的声学训练,以便识别可以将其与通常的工厂噪音区分开来。
另一个例子是,如果您的应用程序被特定群体的人使用,比如一个以编程为主要主题的搜索应用程序。您通常会使用诸如面向对象、dot net或调试之类的词语。这可以通过自定义语言模型来识别。
创建自定义声学模型
要创建自定义声学模型,您需要音频文件和文本记录。每个音频文件必须存储为 WAV 格式,长度在 100 毫秒到 1 分钟之间。建议文件开头和结尾至少有 100 毫秒的静音。通常,这将在 500 毫秒到 1 秒之间。在背景噪音很多的情况下,建议在内容之间有静音。
每个文件应包含一个句子或话语。文件应具有唯一名称,整个文件集可达到 2 GB。这相当于大约 17 到 34 小时的音频,具体取决于采样率。一个集合中的所有文件应放在一个压缩文件夹中,然后可以上传。
随音频文件附带的还有一个包含文本记录的单独文件。该文件应命名,并在名称旁边有句子。文件名和句子应由制表符分隔。
上传音频文件和文本记录将使 CRIS 进行处理。当此过程完成后,您将获得一份报告,说明哪些句子失败或成功。如果任何内容失败,您将获得失败的原因。
当数据集已上传后,您可以创建声学模型。这将与您选择的数据库相关联。当模型创建完成后,您可以开始训练过程。一旦训练完成,您可以部署该模型。
创建自定义语言模型
创建自定义语言模型也需要一个数据集。这个集合是一个包含您模型独特句子或话语的单个纯文本文件。每行新标记一个新话语。最大文件大小为 2 GB。
上传文件后,CRIS 将对其进行处理。一旦处理完成,您将获得一份报告,其中将打印出任何错误及其失败原因。
处理完成后,您可以创建一个自定义语言模型。每个模型都将与您选择的给定数据集相关联。一旦创建,您就可以训练模型,训练完成后,您可以部署它。
部署应用程序
要部署和使用自定义模型,您需要创建一个部署。在这里,您将命名并描述应用程序。您可以选择声学模型和语言模型。请注意,您可以为每个部署的应用程序选择每个模型的一个。
创建部署后,部署过程将开始。这个过程可能需要长达 30 分钟才能完成,所以请耐心等待。部署完成后,您可以通过单击应用程序名称来获取所需信息。您将获得可使用的 URL 以及用于订阅的密钥。
要使用 Bing 语音 API 与自定义模型一起使用,您可以重载CreateDataClientWithIntent和CreateMicrophoneClient。您想要使用的重载将指定主 API 密钥和辅助 API 密钥。您需要使用 CRIS 提供的密钥。此外,您需要指定提供的 URL 作为最后一个参数。
完成此操作后,您可以使用定制的识别模型。
即时翻译语音
使用翻译语音API,您可以添加语音的自动端到端翻译。利用此 API,可以提交语音音频流并检索翻译文本的文本和音频版本。它使用静音检测来检测语音何时结束。一旦检测到暂停,结果将流回。
要获取支持语言的完整列表,请访问以下网站:www.microsoft.com/en-us/translator/business/languages/。
从 API 收到的结果将包含基于音频和文本的结果流。结果包含源语言中的源文本和目标语言中的翻译。
关于如何使用翻译语音API 的详细示例,请访问以下 GitHub 上的示例:github.com/MicrosoftTranslator/SpeechTranslator。
摘要
在本章的整个过程中,我们一直专注于语音。我们首先探讨了如何将语音音频转换为文本,以及将文本转换为语音音频。利用这一点,我们修改了 LUIS 的实现,以便我们可以对智能家居应用程序下达命令并进行对话。从那里,我们继续探讨如何使用语音识别 API 来识别正在说话的人。使用相同的 API,我们还学习了如何验证一个人是否是他们所声称的那个人。我们简要地了解了自定义语音服务的核心功能。最后,我们简要地介绍了翻译语音 API 的入门知识。
在接下来的章节中,我们将回到文本 API,我们将学习如何以不同的方式探索和分析文本。
第六章。理解文本
上一章介绍了语音 API。在本章中,我们将更深入地探讨更多的语言 API。我们将学习如何使用拼写检查功能。然后,我们将发现如何检测文本中的语言、关键词和情感。最后,我们将查看翻译文本 API,看看我们如何检测语言并翻译文本。
到本章结束时,我们将涵盖以下主题:
-
检查拼写和识别俚语和非正式语言、常见名称、同音异义词和品牌
-
在文本中检测语言、关键词和情感
-
在线翻译文本
设置公共核心
在我们深入了解细节之前,我们希望为自己设定成功的基础。在撰写本文时,我们将要涵盖的所有语言 API 都没有 NuGet 客户端包。因此,我们需要直接调用 REST 端点。正因为如此,我们将在事先做一些工作,以确保我们能够通过编写更少的代码来完成。
新项目
我们不会将 API 添加到我们的智能家居应用程序中。按照以下步骤,使用我们在第一章中创建的 MVVM 模板创建一个新项目,使用 Microsoft 认知服务入门:
-
进入 NuGet 包管理器并安装
Newtonsoft.Json。这将帮助我们反序列化 API 响应并序列化请求体。 -
右键单击引用。
-
在程序集选项卡中,选择System.Web和System.Runtime.Serialization。
-
点击确定。
-
在
MainView.xaml文件中,添加一个TabControl元素。我们所有的附加视图都将作为TabItems添加到MainView中。
网络请求
所有 API 都遵循相同的模式。它们使用POST或GET请求调用各自的端点。进一步来说,它们将参数作为查询字符串传递,并将一些作为请求体。由于它们有这些相似之处,我们可以创建一个类来处理所有 API 请求。
在Model文件夹中,添加一个新类,并将其命名为WebRequest。
我们还需要一些private变量,如下所示:
private const string JsonContentTypeHeader = "application/json";
private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings
{
DateFormatHandling = DateFormatHandling.IsoDateFormat,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
private HttpClient _httpClient;
private string _endpoint;
常量JsonContentTypeHeader定义了我们想要用于所有 API 调用的内容类型。_settings短语是一个JsonSerializerSettings对象,它指定了我们想要如何(反)序列化 JSON 数据。
_httpClient是我们将用于发出 API 请求的对象。最后一个成员_endpoint将保存 API 端点。
如以下代码所示,我们的构造函数将接受两个参数:一个用于 URI 的字符串和一个用于 API 密钥的字符串:
public WebRequest(string uri, string apiKey)
{
_endpoint = uri;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey);
}
我们将uri分配给相应的成员。接下来,我们创建一个新的HttpClient类型的对象并添加一个请求头。这是包含给定apiKey的头。
该类将包含一个函数,MakeRequest。这个函数应该有Task<TResponse>的返回类型,这意味着我们在调用函数时指定的类型。正如你在以下代码中所看到的,它应该接受三个参数:一个HttpMethod,一个查询string,以及一个TRequest(这是我们调用时指定的请求体)。该函数应该是异步的:
public async Task <TResponse> MakeRequest <TRequest, TResponse (HttpMethod method, string queryString, TRequest requestBody = default(TRequest))
前面的行显示了完整的函数签名。注意我们不需要指定请求体,因为在某些情况下它可能是空的。我们将在稍后讨论TRequest和TResponse可能是什么。
我们进入一个try子句,如下所示:
try {
string url = $"{_endpoint}{queryString}";
var request = new HttpRequestMessage(method, url);
if (requestBody != null)
request.Content = new StringContent (JsonConvert.SerializeObject(requestBody, _settings), Encoding.UTF8, JsonContentTypeHeader);
HttpResponseMessage response = await _httpClient.SendAsync(request);
首先,我们创建一个url,由我们的_endpoint和queryString组成。使用这个和指定的method,我们创建一个HttpRequestMessage对象。
如果我们有requestBody,我们通过序列化requestBody将Content添加到request对象中。
在请求就绪后,我们向_httpClient对象上的SendAsync进行异步调用。这将调用 API 端点,返回一个包含响应的HttpResponseMessage。
如果response成功,我们希望将Content作为字符串获取。这可以通过以下方式完成:
-
进行异步调用到
ReadAsStringAsync。这将返回一个字符串。 -
将字符串反序列化为
TResponse对象。 -
将反序列化的对象返回给调用者。
如果responseContent中没有数据,我们返回一个默认的TResponse。这将包含所有属性的所有默认值,如下所示:
if (response.IsSuccessStatusCode)
{
string responseContent = null;
if (response.Content != null)
responseContent = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(responseContent))
return JsonConvert.DeserializeObject<TResponse>(responseContent,_settings);
return default(TResponse);
}
如果 API 响应包含任何错误代码,那么我们尝试将错误消息作为字符串(errorObjectString)获取。在典型应用程序中,你可能会想要反序列化它并将其传播给用户。然而,由于这是一个简单的示例应用程序,我们将选择将其输出到以下代码所示的Debug控制台窗口:
else
{
if (response.Content != null && response.Content.Headers.ContentType.MediaType.Contains (JsonContentTypeHeader))
{
var errorObjectString = await response.Content.ReadAsStringAsync();
Debug.WriteLine(errorObjectString);
}
}
确保你添加相应的catch子句,并将任何异常输出到Debug控制台窗口。同时,确保在发生任何异常时返回默认的TResponse。
数据契约
由于我们需要将 JSON 数据序列化和反序列化作为 API 请求和响应的一部分,我们需要创建数据契约。这些将作为TResponse和TRequest对象,用于WebRequest类。
在项目中添加一个名为Contracts的新文件夹。一个典型的数据契约可能看起来像以下这样:
[DataContract]
public class TextErrors {
[DataMember]
public string id { get; set; }
[DataMember]
public string message { get; set; }
}
这与文本分析 API 中的错误相对应。正如你所看到的,它有两个字符串属性id和message。两者都可能出现在 API 响应中。
当讨论每个 API 时,我们将以表格形式或 JSON 格式显示所有请求和响应参数。我们不会查看这些如何转换为数据契约,但它们将采用与之前所示类似的形式。然后取决于你创建所需的契约。
需要注意的最重要的一点是属性名称必须与相应的 JSON 属性完全相同。
在继续之前,请确保代码可以编译并且你可以运行应用程序。
纠正拼写错误
Bing 拼写检查 API 利用机器学习和统计机器翻译的力量来训练和进化一个高度上下文化的拼写检查算法。这样做允许我们利用上下文来进行拼写检查。
一个典型的拼写检查器将遵循基于字典的规则集。正如你可以想象的那样,这将需要不断的更新和扩展。
使用 Bing 拼写检查 API,我们可以识别和纠正俚语和非正式语言。它可以识别常见的命名错误并纠正单词拆分问题。它可以检测并纠正发音相同但意义和拼写不同的单词(同音异义词)。它还可以检测并纠正品牌和流行表达。
在View文件夹中创建一个新的View;调用文件SpellCheckView.xaml。添加一个TextBox元素用于输入查询。我们还需要两个TextBox元素用于前文和后文。添加一个TextBox元素来显示结果和一个Button元素来执行拼写检查。
在名为ViewModel的文件夹中添加一个新的ViewModel;调用文件SpellCheckViewModel.cs。将类设置为public,并让它继承自ObservableObject类。添加以下private成员:
private WebRequest _webRequest;
这是我们在之前创建的WebRequest类。
我们需要与我们的View对应的属性。这意味着我们需要四个string属性和一个ICommand属性。
如果你还没有这样做,请前往portal.azure.com注册一个免费的 API 密钥。
构造函数应如下所示:
public SpellCheckViewModel()
{
_webRequest = new WebRequest ("https://api.cognitive.microsoft.com/bing/v7.0/spellcheck/?", "API_KEY_HERE");
ExecuteOperationCommand = new DelegateCommand(
ExecuteOperation, CanExecuteOperation);
}
我们创建一个新的WebRequest类型的对象,指定 Bing 拼写检查 API 端点和 API 密钥。我们还需要为我们的ExecuteOperationCommand属性创建一个新的DelegateCommand。
CanExecuteOperation属性应该在输入查询填写时返回true,否则返回false。
要执行对 API 的调用,我们执行以下操作:
private async void ExecuteOperation(object obj)
{
var queryString = HttpUtility.ParseQueryString(string.Empty);
queryString["text"] = InputQuery;
queryString["mkt"] = "en-us";
//queryString["mode"] = "proof";
if (!string.IsNullOrEmpty(PreContext)) queryString["preContextText"] = PreContext;
if(!string.IsNullOrEmpty(PostContext))
queryString["postContextText"] = PostContext;
首先,我们使用HttpUtility创建一个queryString。这将格式化字符串,使其可以在 URI 中使用。
由于我们将使用GET方法调用 API,我们需要在字符串中指定所有参数。所需的参数是text和mkt,分别是输入查询和语言。如果我们已经输入了PreContext和/或PostContext,那么我们也添加这些参数。我们将在稍后更详细地查看不同的参数。
要发出请求,我们需要进行以下调用:
SpellCheckResponse response = await _webRequest.MakeRequest <object, SpellCheckResponse>(HttpMethod.Get, queryString.ToString());
ParseResults(response);
}
我们在_webRequest对象上调用MakeRequest。由于我们正在执行GET请求,我们不需要任何请求体,并将object作为TRequest传递。我们期望返回一个SpellCheckResponse合约。这将包含结果数据,我们将在稍后更详细地查看参数。
当我们得到响应时,我们将它传递给一个函数来解析,如下面的代码所示:
private void ParseResults(SpellCheckResponse response)
{
if(response == null || response.flaggedTokens == null || response.flaggedTokens.Count == 0)
{
Result = "No suggestions found";
return;
}
StringBuilder sb = new StringBuilder();
sb.Append("Spell checking results:nn");
如果我们没有收到任何响应,我们将退出函数。否则,我们将创建一个StringBuilder来格式化结果,如下面的代码所示:
foreach (FlaggedTokens tokens in response.flaggedTokens)
{
if (!string.IsNullOrEmpty(tokens.token))
sb.AppendFormat("Token is: {0}n", tokens.token);
if(tokens.suggestions != null || tokens.suggestions.Count != 0)
{
foreach (Suggestions suggestion in tokens.suggestions)
{
sb.AppendFormat("Suggestion: {0} - with score: {1}n", suggestion.suggestion, suggestion.score);
}
sb.Append("n");
}
}
Result = sb.ToString();
如果我们有任何更正的拼写,我们将遍历它们。我们将所有建议添加到StringBuilder中,确保我们添加了建议正确性的可能性。最后,我们确保将结果输出到 UI。
以下表格描述了我们可以添加到 API 调用中的所有参数:
| 参数 | 描述 |
|---|---|
text | 我们想要检查拼写和语法错误的文本。 |
| mode | 当前拼写检查的模式。可以是以下之一:
-
校对:用于长查询的拼写更正,通常用于 MS Word。
-
拼写:用于搜索引擎更正。可用于长度最多九个单词(标记)的查询。
|
preContextText | 为文本提供上下文的字符串。petal 参数是有效的,但如果在此参数中指定 bike,它将被更正为 pedal。 |
|---|---|
postContextText | 为文本提供上下文的字符串。read 参数是有效的,但如果在此参数中指定 carpet,它可能被更正为 red。 |
mkt | 对于校对模式,必须指定语言。目前可以是 en-us、es-es 或 pt-br。对于拼写模式,支持所有语言代码。 |
成功的响应将是一个包含以下内容的 JSON 响应:
{
"_type": "SpellCheck",
"flaggedTokens": [
{
"offset": 5,
"token": "Gatas",
"type": "UnknownToken",
"suggestions": [
{
"suggestion": "Gates",
"score": 1
}]
}]
}
offset是单词在文本中的位置,token是包含错误的单词,而type描述了错误的类型。suggestions短语包含一个包含建议更正及其正确性的概率的数组。
当View和ViewModel已经正确初始化,如前几章所示,我们应该能够编译并运行示例。
运行拼写检查的示例输出可能给出以下结果:
通过文本分析提取信息
使用文本分析API,我们能够分析文本。我们将涵盖语言检测、关键短语分析和情感分析。此外,还有一个新功能是检测主题的能力。然而,这确实需要大量的样本文本,因此我们不会详细介绍这个最后的功能。
对于我们所有的文本分析任务,我们将使用一个新的View。在View文件夹中添加一个新的View,命名为TextAnalysisView.xaml。这应该包含一个用于输入查询的TextBox元素。它还应该有一个用于结果的TextBox元素。我们需要三个Button元素,每个用于我们将执行的一种检测分析。
我们还需要一个新的ViewModel,因此将TextAnalysisViewModel.cs添加到ViewModel文件夹。在这里,我们需要两个string属性,每个TextBox一个。还要添加三个ICommand属性,每个按钮一个。
如果您还没有这样做,请前往portal.azure.com注册 API 密钥。
为WebRequest类型添加一个名为_webRequest的private成员。有了这个,我们就可以创建构造函数,如下面的代码所示:
public TextAnalysisViewModel()
{
_webRequest = new WebRequest("ROOT_URI","API_KEY_HERE");
DetectLanguageCommand = new DelegateCommand(DetectLanguage, CanExecuteOperation);
DetectKeyPhrasesCommand = new DelegateCommand(DetectKeyPhrases, CanExecuteOperation);
DetectSentimentCommand = new DelegateCommand(DetectSentiment, CanExecuteOperation);
}
构造函数创建一个新的WebRequest对象,指定 API 端点和 API 密钥。然后我们继续为我们的ICommand属性创建DelegateCommand对象。CanExecuteOperation函数应该在我们输入查询时返回true,否则返回false。
检测语言
该 API 可以检测超过 120 种不同语言中的文本所使用的语言。
这是一个POST调用,因此我们需要发送请求体。请求体应包含documents。这基本上是一个包含每个text的唯一id的数组。它还需要包含文本本身,如下面的代码所示:
private async void DetectLanguage(object obj)
{
var queryString = HttpUtility.ParseQueryString("languages");
TextRequests request = new TextRequests
{
documents = new List<TextDocumentRequest>
{
new TextDocumentRequest {id="FirstId", text=InputQuery}
}
};
TextResponse response = await _webRequest.MakeRequest<TextRequests, TextResponse>(HttpMethod.Post, queryString.ToString(), request);
我们创建一个queryString,指定我们想要到达的 REST 端点。然后我们继续创建一个包含文档的TextRequest合约。因为我们只想检查一段文本,所以我们添加一个TextDocumentRequest合约,指定一个id和文本。
当请求被创建时,我们调用MakeRequest。我们期望响应为TextResponse类型,请求体为TextRequests类型。我们传递POST作为调用方法、queryString和请求体。
如果响应成功,那么我们将遍历detectedLanguages。我们将语言添加到StringBuilder中,同时也输出该语言正确性的概率。然后这将在 UI 中显示,如下面的代码所示:
if(response.documents == null || response.documents.Count == 0)
{
Result = "No languages was detected.";
return;
}
StringBuilder sb = new StringBuilder();
foreach (TextLanguageDocuments document in response.documents)
{
foreach (TextDetectedLanguages detectedLanguage in document.detectedLanguages)
{
sb.AppendFormat("Detected language: {0} with score {1}n", detectedLanguage.name, detectedLanguage.score);
}
}
Result = sb.ToString();
成功的响应将包含以下 JSON:
{
"documents": [
{
"id": "string",
"detectedLanguages": [
{
"name": "string",
"iso6391Name": "string",
"score": 0.0
}]
}],
"errors": [
{
"id": "string",
"message": "string"
}]
}
这包含一个documents数组-,与请求中提供的数量一样。每个文档将标记一个唯一的id并包含一个detectedLanguage实例的数组。这些语言将具有name、iso6391Name和正确性的概率(score)。
如果任何文档发生错误,我们将得到一个包含errors的数组。每个错误将包含发生错误的文档的id和作为字符串的message。
成功的调用将创建一个类似于以下截图的结果:
从文本中提取关键短语
从文本中提取关键短语可能对我们想要让我们的应用程序知道关键谈话点很有用。使用这个,我们可以了解人们在文章、讨论或其他此类文本来源中讨论的内容。
这个调用也使用POST方法,这需要一个请求体。与语言检测一样,我们需要指定文档。每个文档都需要一个唯一的 ID、文本和使用的语言。在撰写本文时,仅支持英语、德语、西班牙语和日语。
要提取关键短语,我们使用以下代码:
private async void DetectKeyPhrases(object obj)
{
var queryString = HttpUtility.ParseQueryString("keyPhrases");
TextRequests request = new TextRequests
{
documents = new List<TextDocumentRequest>
{
new TextDocumentRequest { id = "FirstId", text = InputQuery, language = "en" }
}
};
TextKeyPhrasesResponse response = await _webRequest.MakeRequest<TextRequests, TextKeyPhrasesResponse>(HttpMethod.Post, queryString.ToString(), request);
如您所见,它与检测语言非常相似。我们使用keyPhrases作为 REST 端点创建一个queryString。我们创建一个TextRequests类型的请求对象。我们添加文档列表,创建一个新的TextDocumentRequest。我们再次需要id和text,但我们还添加了一个language标签,如下面的代码所示:
if (response.documents == null || response.documents?.Count == 0)
{
Result = "No key phrases found.";
return;
}
StringBuilder sb = new StringBuilder();
foreach (TextKeyPhrasesDocuments document in response.documents)
{
sb.Append("Key phrases found:n");
foreach (string phrase in document.keyPhrases)
{
sb.AppendFormat("{0}n", phrase);
}
}
Result = sb.ToString();
如果响应包含任何关键短语,我们将遍历它们并将它们输出到 UI。一个成功的响应将提供以下 JSON:
{
"documents": [{
"keyPhrases": [
"string" ],
"id": "string"
}],
"errors": [
{
"id": "string",
"message": "string"
} ]
}
这里有一个documents数组。每个文档都有一个唯一的id,对应于请求中的 ID。每个文档还包含一个字符串数组,包含keyPhrases。
与语言检测一样,任何错误都会被返回。
学习判断文本是正面还是负面
使用情感分析,我们可以检测文本是否为正面。如果你有一个用户可以提交反馈的商品网站,这个功能可以自动分析反馈是否通常是正面或负面。
情感分数以 0 到 1 之间的数字返回,高数值表示正面情感。
与前两次分析一样,这是一个POST调用,需要请求体。同样,我们需要指定文档,每个文档都需要一个唯一的 ID、文本和语言,如下面的代码所示:
private async void DetectSentiment(object obj)
{
var queryString = HttpUtility.ParseQueryString("sentiment");
TextRequests request = new TextRequests
{
documents = new List<TextDocumentRequest>
{
new TextDocumentRequest { id = "FirstId", text = InputQuery, language = "en" }
}
};
TextSentimentResponse response = await _webRequest.MakeRequest <TextRequests, TextSentimentResponse>(HttpMethod.Post, queryString.ToString(), request);
我们创建一个指向sentiment作为 REST 端点的queryString。数据合约是TextRequests,包含documents。我们传递的文档有一个唯一的id,文本和语言:
调用MakeRequest将需要一个TextSentimentRequests类型的请求体,我们期望结果为TextSentimentResponse类型。
如果响应包含任何documents,我们将遍历它们。对于每个文档,我们检查score,并输出文本是正面还是负面。然后这将在 UI 中显示,如下所示:
if(response.documents == null || response.documents?.Count == 0)
{
Result = "No sentiments detected";
return;
}
StringBuilder sb = new StringBuilder();
foreach (TextSentimentDocuments document in response.documents)
{
sb.AppendFormat("Document ID: {0}n", document.id);
if (document.score >= 0.5)
sb.AppendFormat("Sentiment is positive, with a score of{0}n", document.score);
else
sb.AppendFormat("Sentiment is negative with a score of {0}n", document.score);
}
Result = sb.ToString();
一个成功的响应将导致以下 JSON:
{
"documents": [
{
"score": 0.0,
"id": "string"
}],
"errors": [
{
"id": "string",
"message": "string"
}]
}
这是一个documents数组。每个文档将有一个与请求中 ID 相对应的id,以及情感score。如果发生了任何errors,它们将像我们在语言和关键短语检测部分看到的那样被记录。
一个成功的测试可能看起来像以下这样:
即时翻译文本
使用翻译文本 API,你可以轻松地将翻译添加到你的应用程序中。该 API 允许你自动检测语言。这可以用来提供本地化内容,或快速翻译内容。它还允许我们查找可以用于在不同上下文中翻译单词的替代翻译。
此外,翻译文本 API 可以用来构建定制的翻译系统。这意味着你可以改进现有的模型。这可以通过添加与你的行业中的表达和词汇相关的人类翻译来实现。
翻译文本 API 作为 REST API 提供。我们将介绍你可以到达的四个端点。要使用该 API,应使用以下根 URL:
https://api.cognitive.microsofttranslator.com
在 Microsoft Azure 门户上注册 API 密钥。
翻译文本
要将文本从一种语言翻译成另一种语言,你应该调用以下 URL 路径:
/translate
必须指定以下参数:
To - Language to translate to. Must be specified as two-letter language code.
此参数可以指定多次。
请求体必须包含要翻译的文本。
成功的调用将产生以下 JSON 输出:
[
{
"detectedLanguage": {
"language": "en",
"score": 1.0
},
"translations": [
"text": "Translated text",
"to": "en"
]
}
]
转换文本脚本
要将一种语言脚本(如阿拉伯语)翻译成另一种语言脚本(如拉丁语),你应该调用以下 URL 路径:
/transliterate
必须指定以下参数:
language - two-letter language code of language used in the language script.
fromScript - four-letter code for script language you are translating from.
toScript - four-letter code for script language you are translating to.
请求体必须包含要翻译的文本。
成功的调用将产生以下 JSON 输出:
[
{
"text": "translated text"
"script": "latin"
}
]
与语言一起工作
在处理语言时,你可以使用两个路径。第一个路径用于检测特定文本中的语言。第二个路径用于获取其他 API 支持的语言列表。
识别语言
要检测特定文本使用的语言,你应该调用以下 URL 路径:
/detect
请求体必须包含要翻译的文本。不需要任何参数。
成功的调用将产生以下 JSON 输出:
[
{
"language": "en",
"score": 1.0,
"isTranslationSupported": true,
"isTransliterationSupported": false,
"alternatives": [
{
"language": "pt",
"score": 0.8,
"isTranslationSupported": false
"isTransliterationSupported": false
},
{
"language": "latn",
"score": 0.7,
"isTranslationSupported": true
"isTransliterationSupported": true
}
]
}
]
获取支持的语言
要获取支持的语言列表,你应该调用以下 URL 路径:
/languages
此调用不需要任何参数或请求体。
成功的调用将产生以下 JSON 输出:
[
"translation": {
...
"en": {
"name": "English",
"nativeName": "English",
"dir": "ltr"
},
...
},
"transliteration": {
"ar": {
"name": "Latin",
"nativeName": "",
"scripts": [
{
"code": "Arab",
"name": "Arabic",
"nativeName": "",
"dir": "rtl",
"toScripts": [
{
"code:" "Latn",
"name": "Latin",
"nativeName": "",
"dir": "ltr"
}
]
},
{
"code": "Latn",
"name": "Latin",
"nativeName": "",
"dir": "ltr",
"toScripts": [
{
"code:" "Arab",
"name": "Arabic",
"nativeName": "",
"dir": "rtl"
}
]
}
]
},
...
},
"dictionary": {
"af": {
"name": "Afrikaans",
"nativeName": "Afrikaans",
"dir": "ltr",
"translations": [
{
"name": "English",
"nativeName": "English",
"dir": "ltr",
"code": "en"
}
...
]
}
...
}
]
如你所见,两字母国家代码是每个条目的关键。你还可以找到每个转写语言的四字母代码。此 API 路径可以用作其他 API 路径的基础。
摘要
在本章中,我们专注于语言 API。我们首先创建了执行对不同服务 API 调用的所需部分。随后,我们查看了 Bing 拼写检查 API。然后,我们转向更分析的 API,学习了如何检测语言、关键词和情感。最后,我们探讨了如何使用翻译文本 API。
下一章将带我们从语言 API 到知识 API。在接下来的章节中,我们将学习如何根据上下文识别和识别实体。此外,我们还将学习如何使用推荐 API。