微软认知服务学习指南-全-
微软认知服务学习指南(全)
原文:
annas-archive.org/md5/d574e5ef7c17b4f651431caa0b5defbd译者:飞龙
前言
人工智能和机器学习是复杂的话题,将此类功能添加到应用程序中在历史上需要大量的处理能力,更不用说巨大的学习量。微软认知服务的引入为开发者提供了轻松添加这些功能的可能性。它使我们能够创建更智能、更类似人类的程序。
本书旨在教你如何利用微软认知服务中的 API。你将了解每个 API 能提供什么,以及如何将其添加到你的应用程序中。你将看到不同的 API 调用对输入数据的要求以及你可以期待得到什么。本书中的大多数 API 都包含了理论和实践示例。
本书旨在帮助你入门。它侧重于展示如何使用微软认知服务,同时考虑到当前的最佳实践。本书的目的不是展示高级用例,而是为你提供一个起点,让你自己开始尝试使用这些 API。
本书面向的对象
本书面向有一定编程经验的.NET 开发者。假设你知道如何进行基本的编程任务以及如何在 Visual Studio 中导航。阅读本书不需要具备人工智能或机器学习的先验知识。
理解网页请求的工作原理是有益的,但并非必需。
本书涵盖的内容
第一章,开始使用微软认知服务,通过描述其提供的内容和提供一些基本示例来介绍微软认知服务。
第二章,分析图像以识别面部,涵盖了大多数图像 API,介绍了面部识别和识别、图像分析、光学字符识别等。
第三章,分析视频,介绍了视频索引器 API。
第四章,让应用程序理解命令,深入探讨了如何设置语言理解智能服务(LUIS),以便你的应用程序能够理解最终用户的意图。
第五章,与你的应用程序对话,深入探讨了不同的语音 API,包括文本到语音和语音到文本的转换、说话人识别和识别以及识别自定义说话风格和环境。
第六章,理解文本,介绍了一种分析文本的不同方法,利用强大的语言分析工具以及更多。
第七章,为企业构建推荐系统,涵盖了推荐 API。
第八章, 以自然方式查询结构化数据,处理了学术论文和期刊的探索。通过本章,我们探讨了如何使用 Academic API 并自行设置类似的服务。
第九章, 添加专业搜索,深入探讨了 Bing 的不同搜索 API。这包括新闻、网页、图像和视频搜索以及自动建议。
第十章, 连接各个部分,将几个 API 连接起来,并通过查看一些自然步骤来结束本书。
附录 A, LUIS 实体,列出了所有预构建的 LUIS 实体的完整列表。
附录 B, 许可信息,展示了示例代码中使用的所有第三方库的相关许可信息。
要充分利用这本书
-
要跟随本书中的示例,您将需要 Visual Studio 2015 社区版或更高版本。您还需要一个有效的互联网连接和 Microsoft Azure 订阅;试用订阅也可以。
-
要完整体验示例,您应该能够访问网络摄像头,并且将扬声器和麦克风连接到计算机;然而,这两者都不是强制性的。
下载示例代码文件
您可以从www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在
www.packtpub.com登录或注册。 -
选择支持选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载完成后,请确保您使用最新版本的软件解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learning-Microsoft-Cognitive-Services-Third-Edition。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789800616_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如;“当我们把一些内容放入DelegateCommand.cs文件时,可以实现这一点。”
代码块设置如下:
private string _filePath;
private IFaceServiceClient _faceServiceClient;
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
private string _filePath;
private IFaceServiceClient _faceServiceClient;
粗体:表示新术语、重要单词或您在屏幕上看到的单词,例如在菜单或对话框中,这些单词在文本中也会以这种方式出现。例如:“打开 Visual Studio 并选择文件 | 新建 | 项目。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小贴士和技巧看起来是这样的。
联系我们
读者的反馈总是受欢迎的。
一般反馈:请发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。如果您对本书的任何方面有疑问,请通过电子邮件联系我们 <questions@packtpub.com>。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们非常感谢您能提供位置地址或网站名称。请通过电子邮件联系我们 <copyright@packtpub.com> 并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需更多关于 Packt 的信息,请访问 packtpub.com。
第一章. Microsoft Cognitive Services 入门
您刚刚开始学习关于 Microsoft Cognitive Services 的道路。本章将作为对其提供的服务的一个温和的介绍。最终目标是更好地了解这些认知服务 API 能为您做什么。到本章结束时,我们将创建一个易于使用的项目模板。您将学会如何在图像中检测人脸,并且会听到人脸数量的反馈。
在本章中,我们将涵盖以下主题:
-
已经使用 Microsoft Cognitive Services 的应用程序
-
创建模板项目
-
使用 Face API 在图像中检测人脸
-
探索 Microsoft Cognitive Services 能提供什么
-
使用 Bing Speech API 进行文本到语音的转换
认知服务在实际中的应用,用于娱乐和改变生活
介绍 Microsoft Cognitive Services 的最佳方式是看看它如何在实际中使用。微软(以及其他公司)已经创建了许多示例应用程序来展示其功能。其中一些可能看起来很傻,比如 How-Old.net (how-old.net/) 图像分析和“如果我变成那个人”应用程序。这些应用程序已经引起了很多关注,并以良好的方式展示了 API 的一些功能。
真正令人鼓舞的一个演示是关于一个视力受损的人。会说话的电脑激发了他创建一个应用程序,让盲人和视力受损的人能够理解周围发生的事情。该应用程序是基于 Microsoft Cognitive Services 构建的。它让我们很好地了解这些 API 如何改变世界,使之变得更好。在继续之前,请访问www.youtube.com/watch?v=R2mC-NUAmMk并一窥 Microsoft Cognitive Services 的世界。
设置样板代码
在我们开始深入行动之前,我们将进行一些初始设置。更具体地说,我们将设置一些样板代码,我们将在整本书中利用这些代码。
要开始,您需要安装 Visual Studio 的一个版本,最好是 Visual Studio 2015 或更高版本。社区版就足够用于这个目的了。您不需要比默认安装提供的内容更多。
注意
您可以在www.microsoft.com/en-us/download/details.aspx?id=48146找到 Visual Studio 2017。
在整本书中,我们将利用不同的 API 构建一个智能家居应用程序。该应用程序旨在展示未来房屋可能的样子。如果您看过《钢铁侠》电影,您可以将该应用程序想象成某种形式的 Jarvis。
此外,我们还将使用认知服务 API 创建较小的示例应用程序。这样做将允许我们查看每个 API,即使是那些没有进入最终应用程序的 API。
我们将要构建的所有应用程序的共同点在于它们将是Windows Presentation Foundation(WPF)应用程序。这是相当知名的,并允许我们使用模型-视图-视图模型(MVVM)模式来构建应用程序。走这条路的一个优点是我们将能够清楚地看到 API 的使用情况。它还使代码分离,这样你可以轻松地将 API 逻辑带到其他应用程序中。
以下步骤描述了创建新的 WPF 项目的过程:
-
打开 Visual Studio,选择文件 | 新建 | 项目。
-
在对话框中,从模板 | Visual C#中选择WPF 应用程序选项,如下所示:
![设置模板代码]()
-
删除
MainWindow.xaml文件,并创建以下截图所示的文件和文件夹:![设置模板代码]()
我们不会详细讲解 MVVM 模式,因为这超出了本书的范围。从截图中的关键点来看,我们已经将View与逻辑部分分离。然后我们依赖ViewModel来连接这些部分。
注意
如果你想了解更多关于 MVVM 的信息,我建议阅读www.codeproject.com/Articles/100175/Model-View-ViewModel-MVVM-Explained。
然而,为了能够运行这个程序,我们确实需要设置我们的项目。按照以下步骤进行:
-
打开
App.xaml文件,确保StartupUri设置为正确的View,如下所示(类名和命名空间可能根据应用程序的名称而变化):<Application x:Class="Chapter1.App" xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="View/MainView.xaml"> -
打开
MainViewModel.cs文件,并让它继承自ObservableObject类。 -
打开
MainView.xaml文件,并将MainViewModel文件作为DataContext添加到其中,如下所示(命名空间和类名可能根据应用程序的名称而变化):<Window x:Class="Chapter1.View.MainView" xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/ expression/blend/2008" mc:Ignorable="d" Title="Chapter 1" Height="300" Width="300"> <Window.DataContext> <viewmodel:MainViewModel /> </Window.DataContext>
接下来,我们需要填写ObservableObject.cs文件的内容。我们首先让它继承自INotifyPropertyChanged类,如下所示:
public class ObservableObject : INotifyPropertyChanged
这是一个相当小的类,应该包含以下内容:
public event PropertyChangedEventHandlerPropertyChanged;
protected void RaisePropertyChangedEvent(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
我们声明一个属性更改事件并创建一个函数来触发事件。这将允许用户界面(UI)在给定属性更改时更新其值。
我们还需要能够在按钮点击时执行操作。这可以通过在DelegateCommand.cs文件中放入一些内容来实现。首先让这个类继承自ICommand类,并声明以下两个变量:
public class DelegateCommand : ICommand
{
private readonly Predicate<object> _canExecute;
private readonly Action<object> _execute;
我们创建的两个变量将在构造函数中设置。正如你将注意到的,你不需要添加_canExecute参数,你很快就会明白原因:
public DelegateCommand(Action<object> execute, Predicate<object> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
要完成类,我们添加两个public函数和一个public事件,如下所示:
public bool CanExecute(object parameter)
{
if (_canExecute == null) return true;
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandlerCanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
}
声明的函数将返回构造函数中声明的相应谓词或动作。这将是我们在ViewModel实例中声明的,反过来,这将执行动作或告诉应用程序它是否可以执行动作。如果一个按钮处于禁用状态(即当CanExecute函数返回false时),并且CanExecute函数的状态发生变化,声明的该事件将让按钮知道。
在设置好之后,你应该能够编译并运行应用程序,所以继续尝试吧。你会注意到应用程序实际上并没有做任何事情或展示任何数据,但我们有一个非常好的起点。
在我们对代码进行任何其他操作之前,我们将按照以下步骤将项目导出为模板。这样做是为了我们不必为每个创建的小型样本项目重复所有这些步骤:
-
将命名空间名称替换为替代参数:
-
在所有
.cs文件中,将命名空间名称替换为$safeprojectname$ -
在所有
.xaml文件中,将项目名称替换为$safeprojectname$(通常为类名和命名空间声明) -
导航到文件 | 导出模板。这将打开导出模板向导,如下截图所示:
![设置样板代码]()
-
点击项目模板按钮。选择我们刚刚创建的项目,然后点击下一步按钮。
-
只需将图标和预览图像留空。输入一个可识别的名称和描述。点击完成按钮:
![设置样板代码]()
-
模板现在已导出为
.zip文件并存储在指定的位置。
默认情况下,模板将再次导入到 Visual Studio 中。我们将通过创建本章的项目来测试它是否立即工作。所以请继续创建一个新的项目,选择我们刚刚创建的模板。该模板应列在已安装模板列表的Visual C#部分中。将项目命名为Chapter1或你喜欢的其他名称。确保在进入下一步之前编译并能够运行它。
使用面部 API 检测面部
使用新创建的项目,我们现在将尝试我们的第一个 API,即面部 API。我们不会做很多,但我们仍然会看到检测图像中的面部是多么简单。
为了做到这一点,我们需要遵循以下步骤:
-
在 Microsoft Azure 注册面部 API 预览订阅
-
将必要的NuGet包添加到我们的项目中
-
向应用程序添加 UI
-
按命令检测面部
转到portal.azure.com开始注册 Face API 免费订阅的过程。您将被带到登录页面。使用您的 Microsoft 账户登录;如果您没有,则需要注册一个。
登录后,您需要通过点击右侧菜单中的+ 新建来添加一个新的资源。搜索Face API并选择第一个条目,如下截图所示:

输入一个名称并选择订阅、位置和定价层。在撰写本文时,有两种定价选项,一个是免费的,一个是付费的,如下截图所示:

一旦创建,您就可以进入新创建的资源。您需要两个可用的 API 密钥之一。这些可以在资源管理菜单的密钥选项中找到,如下截图所示:

我们将要介绍的一些 API 有自己的 NuGet 包。每当这种情况发生时,我们将利用这些包来执行我们想要执行的操作。所有 API 的共同特点是它们都是 REST API,这意味着在实践中,您可以使用任何语言来使用它们。对于没有自己的 NuGet 包的 API,我们直接通过 HTTP 调用 API。
对于我们现在使用的 Face API,确实存在一个 NuGet 包,因此我们需要将其添加到我们的项目中。转到我们之前创建的项目中的NuGet 包管理器选项。在浏览选项卡中,搜索Microsoft.ProjectOxford.Face包,并从 Microsoft 安装该包,如下截图所示:

如您所注意到的,另一个包也将被安装。这是 Face API 所需的Newtonsoft.Json包。
下一步是向我们的应用程序添加 UI。我们将在MainView.xaml文件中添加这个。这意味着我们有DataContext,可以为我们的元素创建绑定,我们现在将定义这些元素。
首先,我们添加一个网格并定义一些网格行,如下所示:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="20" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
定义了三行。第一行是我们将放置图片的行,第二行是状态消息的行,最后一行我们将放置一些按钮。
接下来,我们添加我们的image元素,如下所示:
<Image x:Name="FaceImage" Stretch="Uniform" Source=
"{Binding ImageSource}" Grid.Row="0" />
我们给它起了一个独特的名字。通过将Stretch参数设置为Uniform,我们确保图片保持其宽高比。进一步地,我们将此元素放置在第一行。最后,我们将图片源绑定到ViewModel中的BitmapImage,我们稍后会看到。
下一行将包含一个带有一些状态文本的文本块。Text属性将绑定到ViewModel中的字符串属性,如下所示:
<TextBlockx:Name="StatusTextBlock" Text=
"{Binding StatusText}" Grid.Row="1" />
最后一行将包含一个用于浏览图像的按钮和一个用于检测面部的按钮。两个按钮的command属性将绑定到ViewModel中的DelegateCommand属性,如下所示:
<Button x:Name = "BrowseButton"
Content = "Browse" Height="20" Width="140"
HorizontalAlignment = "Left"
Command="{Binding BrowseButtonCommand}"
Margin="5, 0, 0, 5"Grid.Row="2" />
<Button x:Name="DetectFaceButton"
Content="Detect face" Height="20" Width="140"
HorizontalAlignment="Right"
Command="{Binding DetectFaceCommand}"
Margin="0, 0, 5, 5"Grid.Row="2"/>
在View就绪后,确保代码编译并运行它。这应该向您展示以下 UI:

流程的最后部分是在我们的ViewModel中创建绑定属性,并使按钮执行某些操作。打开MainViewModel.cs文件。该类应该已经继承自ObservableObject类。首先,我们定义以下两个变量:
private string _filePath;
private IFaceServiceClient _faceServiceClient;
string变量将保存我们图像的路径,而IFaceServiceClient变量用于与面部 API 接口。接下来,我们定义两个属性,如下所示:
private BitmapImage _imageSource;
public BitmapImageImageSource
{
get { return _imageSource; }
set
{
_imageSource = value;
RaisePropertyChangedEvent("ImageSource");
}
}
private string _statusText;
public string StatusText
{
get { return _statusText; }
set
{
_statusText = value;
RaisePropertyChangedEvent("StatusText");
}
}
我们这里有一个BitmapImage属性,映射到View中的Image元素。我们还有一个string属性用于状态文本,映射到View中的文本块元素。您可能也注意到,当设置任一属性时,我们调用RaisePropertyChangedEvent事件。这将确保当任一属性有新值时,UI 会更新。
接下来,我们定义我们的两个DelegateCommand对象,并在构造函数中执行一些初始化,如下所示:
public ICommandBrowseButtonCommand { get; private set; }
public ICommandDetectFaceCommand { get; private set; }
public MainViewModel()
{
StatusText = "Status: Waiting for image...";
_faceServiceClient = new FaceServiceClient("YOUR_API_KEY_HERE", "ROOT_URI);
BrowseButtonCommand = new DelegateCommand(Browse);
DetectFaceCommand = new DelegateCommand(DetectFace, CanDetectFace);
}
命令的属性是public以获取,但private以设置。这意味着我们只能从ViewModel内部设置它们。在我们的构造函数中,我们首先设置状态文本。接下来,我们创建一个面部 API 的对象,该对象需要使用我们之前获得的 API 密钥创建。此外,它需要指定根 URI,指向服务位置。例如,如果服务位于西欧,可以是westeurope.api.cognitive.microsoft.com/face/v1.0。
如果服务位于西美国,您将用westus替换westeurope。根 URI 可以在 Azure 门户的以下位置找到:

最后,我们为我们的命令属性创建DelegateCommand构造函数。注意browse命令没有指定谓词。这意味着始终可以点击相应的按钮。为了使代码编译,我们需要创建DelegateCommand构造函数中指定的函数:Browse、DetectFace和CanDetectFace函数。
我们通过创建一个OpenFileDialog对象来开始Browse函数。此对话框分配了一个 JPEG 图像过滤器,然后打开,如下面的代码所示。当对话框关闭时,我们检查结果。如果对话框被取消,我们简单地停止进一步执行:
private void Browse(object obj)
{
var openDialog = new Microsoft.Win32.OpenFileDialog();
openDialog.Filter = "JPEG Image(*.jpg)|*.jpg";
bool? result = openDialog.ShowDialog();
if (!(bool)result) return;
对话框关闭后,我们获取所选文件的文件名,并从它创建一个新的 URI,如下面的代码所示:
_filePath = openDialog.FileName;
Uri fileUri = new Uri(_filePath);
使用新创建的 URI,我们想要创建一个新的 BitmapImage。我们指定它不使用缓存,并设置我们创建的 URI 的 URI 源,如下面的代码所示:
BitmapImage image = new BitmapImage(fileUri);
image.CacheOption = BitmapCacheOption.None;
image.UriSource = fileUri;
最后一步是将位图图像分配给我们的 BitmapImage 属性,以便在 UI 中显示图像。我们还更新状态文本,让用户知道图像已被加载,如下面的代码所示:
ImageSource = image;
StatusText = "Status: Image loaded...";
}
CanDetectFace 函数检查是否应该启用 DetectFacesButton 按钮。在这种情况下,它检查我们的图像属性是否实际上有一个 URI。如果有,那么这意味着我们有一个图像,我们应该能够检测到人脸,如下面的代码所示:
private boolCanDetectFace(object obj)
{
return !string.IsNullOrEmpty(ImageSource?.UriSource.ToString());
}
我们的 DetectFace 方法调用一个 async 方法来上传和检测人脸。返回值包含 FaceRectangles 变量的数组。这个数组包含给定图像中所有人脸位置的矩形区域。我们稍后会查看将要调用的函数。
调用完成后,我们将打印包含人脸数量的行到调试控制台窗口,如下所示:
private async void DetectFace(object obj)
{
FaceRectangle[] faceRects = await UploadAndDetectFacesAsync();
string textToSpeak = "No faces detected";
if (faceRects.Length == 1)
textToSpeak = "1 face detected";
else if (faceRects.Length> 1)
textToSpeak = $"{faceRects.Length} faces detected";
Debug.WriteLine(textToSpeak);
}
在 UploadAndDetectFacesAsync 函数中,我们根据以下代码从图像创建一个 Stream。这个流将被用作调用 Face API 服务的实际输入:
private async Task<FaceRectangle[]>UploadAndDetectFacesAsync()
{
StatusText = "Status: Detecting faces...";
try
{
using (Stream imageFileStream = File.OpenRead(_filePath))
以下行是调用 Face API 检测端点的实际调用:
Face[] faces = await _faceServiceClient.DetectAsync(imageFileStream, true, true, new List<FaceAttributeType>() { FaceAttributeType.Age });
第一个参数是我们之前创建的文件流。其余参数都是可选的。第二个参数应该为 true,如果您想获取人脸 ID。下一个参数指定您是否想接收人脸特征点。最后一个参数接受您可能想要接收的面部属性列表。在我们的例子中,我们希望返回 age 参数,因此我们需要指定它。
此函数调用的返回类型是一个包含所有您指定参数的人脸数组,如下面的代码所示:
List<double> ages = faces.Select(face =>face.FaceAttributes.Age).ToList();
FaceRectangle[] faceRects = faces.Select(face =>face.FaceRectangle).ToArray();
StatusText = "Status: Finished detecting faces...";
foreach(var age in ages) {
Console.WriteLine(age);
}
return faceRects;
}
}
第一行遍历所有人脸并检索所有人脸的大致年龄。这稍后将在 foreach 循环中打印到调试控制台窗口。
第二行遍历所有人脸并检索人脸矩形,包括所有人脸的矩形位置。这是我们返回给调用函数的数据。
添加一个 catch 子句来完成方法。在我们的 API 调用中抛出异常时,我们捕获它。我们想显示错误消息并返回一个空的 FaceRectangle 数组。
在放置了上述代码后,您现在应该能够运行完整的示例。最终结果将类似于以下截图:

调试控制台窗口将打印以下文本:
1 face detected
23,7
不同 API 的概述
现在您已经看到了如何检测面部的基本示例,是时候学习一下认知服务还能为您做什么了。当使用认知服务时,您有 21 个不同的 API 可供选择。这些 API 根据它们的功能被分为五个顶级领域。这些领域是视觉、语音、语言、知识和搜索。我们将在接下来的章节中了解更多关于它们的信息。
视觉
带有视觉标志的 API 允许您的应用程序理解图像和视频内容。它们允许您检索有关面部、情感和其他视觉内容的信息。您可以稳定视频并识别名人。您可以在图像中读取文本并从视频和图像中生成缩略图。
视觉领域包含四个 API,我们现在将探讨它们。
计算机视觉
使用计算机视觉API,您可以从图像中检索可操作信息。这意味着您可以识别内容(例如图像格式、图像大小、颜色、面部等)。您可以检测图像是否为成人/色情。此 API 可以识别图像中的文本并将其提取为机器可读的单词。它可以识别来自各个领域的名人。最后,它还可以生成具有智能裁剪功能的存储高效的缩略图。
我们将在第二章分析图像以识别面部中探讨计算机视觉。
面部
我们已经看到了 Face API 的一个非常基本的示例。其余的 API 主要围绕照片中面部检测、识别、组织和标记进行。除了面部检测,您还可以看到两个面部属于同一个人的可能性有多大。您可以识别面部并找到相似外观的面部。我们还可以使用 API 来识别图像中的情感。
我们将在第二章分析图像以识别面部中进一步探讨 Face API。
视频索引器
使用视频索引器API,您可以在上传后立即开始索引视频。这意味着您可以在不使用专家或自定义代码的情况下获取视频洞察力。利用此 API 强大的人工智能,可以提高内容发现能力。这使得您的内容更容易被发现。
视频索引器 API 将在第三章分析视频中进行更详细的介绍。
内容审核员
内容审核员API 利用机器学习自动审核内容。它可以检测超过 100 种语言的潜在冒犯性和不受欢迎的图像、视频和文本。此外,它还允许您审查检测到的材料以改进服务。
内容审核员将在第二章分析图像以识别面部中进行介绍。
定制视觉服务
定制视觉服务 允许您将您自己的标记图像上传到视觉服务。这意味着您可以将特定于您领域的图像添加到视觉服务中,以便使用计算机视觉 API 进行识别。
定制视觉服务将在 第二章 分析图像以识别面部 中更详细地介绍。
语音
添加其中一个语音 API 允许您的应用程序听到并与其用户对话。这些 API 可以过滤噪音并识别说话人。基于识别的意图,它们可以在您的应用程序中驱动进一步的行动。
语音领域包含三个 API,将在以下部分中概述。
Bing Speech
将 Bing Speech API 添加到您的应用程序中,允许您将语音转换为文本,反之亦然。您可以通过利用麦克风或其他实时源或将音频文件转换为文本来实现将语音音频转换为文本。该 API 还提供语音意图识别,这是由 语言理解智能服务(LUIS)训练的,以理解意图。
说话人识别
说话人 识别 API 使您的应用程序能够知道谁在说话。通过使用此 API,您可以验证说话的人就是他们所声称的人。您还可以根据所选的说话人群体确定未知说话人。
翻译语音 API
翻译语音 API 是一个基于云的自动语音翻译服务。使用此 API,您可以在 Web 应用程序、移动应用程序和桌面应用程序中添加端到端翻译。根据您的用例,它可以为您提供部分翻译、完整翻译和翻译的转录本,涵盖所有与语音相关的 API,在 第五章 与应用程序对话 中。
语言
与语言领域相关的 API 允许您的应用程序处理自然语言并学习如何识别用户的需求。您可以为您的应用程序添加文本和语言分析,以及自然语言理解。
以下五个 API 可在语言领域找到。
Bing 拼写检查
Bing 拼写检查 API 允许您为您的应用程序添加高级拼写检查。
此 API 将在 第六章 理解文本 中进行介绍。
语言理解智能服务 (LUIS)
LUIS 是一个可以帮助您的应用程序理解用户命令的 API。使用此 API,您可以创建理解意图的语言模型。通过使用 Bing 和 Cortana 的模型,您可以使得这些模型能够识别常见的请求和实体(如地点、时间和数字)。您可以为您的应用程序添加对话智能。
LUIS 将在 第四章 让应用程序理解命令 中进行介绍。
文本分析
文本分析API 将帮助您从文本中提取信息。您可以使用它来查找文本的情感(文本是正面还是负面),并且还将能够检测语言、主题、关键词和实体,这些内容将在第六章理解文本中涵盖文本分析 API。
翻译文本 API
通过添加翻译文本API,您可以为超过 60 种语言获得文本翻译。它可以自动检测语言,并且您可以根据需要自定义 API。此外,您可以通过创建用户组,利用众包的力量来提高翻译质量。
翻译文本 API在本书中不会介绍。
知识
当我们谈论知识API 时,我们指的是允许您利用丰富知识的 API。这可能来自网络或学术界,或者可能是您自己的数据。使用这些 API,您将能够探索知识的不同细微差别。
以下四个 API 包含在知识 API 领域。
项目学术知识
使用项目学术知识API,您可以探索学术论文、期刊和作者之间的关系。此 API 允许您解释自然语言用户查询字符串,这使得您的应用程序能够预测用户正在输入的内容。它将评估正在输入的内容并返回学术知识实体。
本 API 将在第八章以自然方式查询结构化数据中详细介绍。
知识探索
知识探索API 将允许您在项目中添加使用交互式搜索结构化数据的功能。它解释自然语言查询并提供自动完成以减少用户工作量。根据接收到的查询表达式,它将检索有关匹配对象的详细信息。
关于此 API 的详细信息将在第八章以自然方式查询结构化数据中介绍。
推荐解决方案
推荐解决方案API 允许您为您的客户提供个性化的产品推荐。您可以使用此 API 向您的应用程序添加经常一起购买的功能。您还可以添加另一个功能,即项目到项目的推荐,这允许客户看到其他客户喜欢的内容。此 API 还将允许您根据客户的先前活动添加推荐。
我们将在第七章为企业构建推荐系统中介绍此 API。
QnA Maker
QnA Maker 是一个用于提炼常见问题(FAQ)信息的服务。使用现有的 FAQ,无论是在线的还是文档中的,您都可以创建问题和答案对。可以对这些对进行编辑、删除和修改,并且您可以添加几个类似的问题以匹配给定的对。
我们将在 第八章 以自然方式查询结构化数据 中介绍 QnA Maker。
项目自定义决策服务
项目自定义决策服务 是一个旨在使用强化学习来个性化内容的服务。该服务理解任何上下文,并能提供基于上下文的内容。
本书不涉及项目自定义决策服务。
搜索
搜索 API 使您能够利用 Bing 的力量使您的应用程序更加智能。使用这些 API,您可以通过单次调用访问来自数十亿网页、图片、视频和新闻文章的数据。
搜索域包含以下 API。
Bing 网络搜索
使用 Bing 网络搜索,您可以在 Bing 索引的数十亿网络文档中搜索详细信息。所有结果都可以根据您指定的布局进行排列和排序,并且结果会根据最终用户的地理位置进行定制。
Bing 网络搜索将在 第九章 添加专业搜索 中介绍。
Bing 图片搜索
使用 Bing 图片搜索 API,您可以将高级图片和元数据搜索添加到您的应用程序中。结果包括图片的 URL、缩略图和元数据。您还将能够获取机器生成的标题、相似图片等。此 API 允许您根据图片类型、布局、新鲜度(图片的新旧程度)和许可证筛选结果。Bing 图片搜索将在 第九章 添加专业搜索 中介绍。
Bing 视频搜索
Bing 视频搜索 将允许您搜索视频并返回丰富结果。结果可能包含视频的元数据、静态或基于运动的缩略图以及视频本身。您可以根据新鲜度、视频长度、分辨率和价格对结果添加过滤器。
Bing 视频搜索将在 第九章 添加专业搜索 中介绍。
Bing 新闻搜索
如果您将 Bing 新闻搜索 添加到您的应用程序中,您可以搜索新闻文章。结果可以包括权威图片、相关新闻和类别、提供者信息、URL 等更多信息。具体来说,您可以根据主题对新闻进行筛选。
Bing 新闻搜索将在 第九章 添加专业搜索 中介绍。
Bing 自动完成
Bing 自动完成 API 是一个小巧但功能强大的 API。它将允许您的用户通过搜索建议更快地搜索,使您能够将强大的搜索功能连接到您的应用程序中。
必应自动完成将在第九章添加专业搜索中介绍。
必应视觉搜索
使用必应视觉搜索API,您可以识别和分类图像。您还可以获取有关图像的知识。
必应视觉搜索将在第九章添加专业搜索中介绍。
必应自定义搜索
通过使用必应自定义搜索API,您可以创建一个强大、定制的搜索,满足您的需求。这个工具是一个无广告的商业工具,允许您提供您想要的搜索结果。
必应自定义搜索将在第九章添加专业搜索中介绍。
必应实体搜索
使用必应实体搜索API,您可以增强您的搜索。该 API 将根据您的搜索词找到最相关的实体。它将找到诸如名人、地点、电影等实体。
我们将不会在本书中介绍必应实体搜索。
获取检测到的面部反馈
现在我们已经看到了微软认知服务还能提供什么,我们将向我们的面部检测应用程序添加一个 API。在本节中,我们将添加必应语音 API,使应用程序能够大声说出面部数量。
此 API 功能在 NuGet 包中未提供,因此我们将使用 REST API。
为了达到我们的最终目标,我们将添加两个新的类,TextToSpeak和Authentication。第一个类将负责生成正确的头信息并调用我们的服务端点。后者类将负责生成认证令牌。这些将在我们的ViewModel中结合在一起,我们将使应用程序能够对我们说话。
我们首先需要获取一个 API 密钥。前往微软 Azure 门户。为必应语音创建一个新的服务。
为了能够调用必应语音 API,我们需要一个授权令牌。回到 Visual Studio,创建一个名为Authentication.cs的新文件。将其放在Model文件夹中。
我们需要向项目添加两个新的引用。在添加引用窗口的组件选项卡中找到System.Runtime.Serialization和System.Web包,并将它们添加。
在我们的Authentication类中,定义四个private变量和一个public属性,如下所示:
private string _requestDetails;
private string _token;
private Timer _tokenRenewer;
private const int TokenRefreshInterval = 9;
public string Token { get { return _token; } }
构造函数应接受一个字符串参数,clientSecret。clientSecret参数是您注册的 API 密钥。
在构造函数中,按照以下方式分配_clientSecret变量:
_clientSecret = clientSecret;
创建一个名为Initialize的新函数,如下所示:
public async Task Initialize()
{
_token = GetToken();
_tokenRenewer = new Timer(new TimerCallback(OnTokenExpiredCallback), this,
TimeSpan.FromMinutes(TokenRefreshInterval),
TimeSpan.FromMilliseconds(-1));
}
然后,我们在稍后创建的方法中获取访问令牌。
最后,我们创建我们的timer类,它将在九分钟后调用callback函数。callback函数需要再次获取访问令牌并将其分配给_token变量。它还需要确保我们在九分钟后再次运行计时器。
接下来,我们需要创建GetToken方法。此方法应返回一个Task<string>对象,并且应声明为private并标记为async。
在方法中,我们首先创建一个HttpClient对象,指向将生成令牌的端点。我们指定根端点并添加令牌发行路径,如下所示:
using(var client = new HttpClient())
{
client.DefaultRequestHeaders.Add ("Opc-Apim-Subscription-Key", _clientSecret);
UriBuilder uriBuilder = new UriBuilder (https://api.cognitive.microsoft.com/sts/v1.0");
uriBuilder.Path = "/issueToken";
然后,我们继续进行 POST 调用以生成令牌,如下所示:
var result = await client.PostAsync(uriBuilder.Uri.AbsoluteUri, null);
当请求发送后,我们期望有一个响应。我们想要读取这个响应并返回响应字符串:
return await result.Content.ReadAsStringAsync();
如果您还没有这样做,请添加一个名为TextToSpeak.cs的新文件。将此文件放在Model文件夹中。
在新创建的类(但位于命名空间内部)下方,我们想要添加两个事件参数类。这些将被用来处理音频事件,我们将在后面看到。
AudioEventArgs类简单地接受一个泛型stream,如下面的代码所示。你可以想象它被用来将音频流发送到我们的应用程序:
public class AudioEventArgs : EventArgs
{
public AudioEventArgs(Stream eventData)
{
EventData = eventData;
}
public StreamEventData { get; private set; }
}
下一个类允许我们发送带有特定错误消息的事件:
public class AudioErrorEventArgs : EventArgs
{
public AudioErrorEventArgs(string message)
{
ErrorMessage = message;
}
public string ErrorMessage { get; private set; }
}
我们继续开始TextToSpeak类的编写,首先声明一些事件和类成员,如下所示:
public class TextToSpeak
{
public event EventHandler<AudioEventArgs>OnAudioAvailable;
public event EventHandler<AudioErrorEventArgs>OnError;
private string _gender;
private string _voiceName;
private string _outputFormat;
private string _authorizationToken;
private AccessTokenInfo _token;
private List<KeyValuePair<string, string>> _headers = new List<KeyValuePair<string, string>>();
类中的前两行是使用我们之前创建的事件参数类的事件。如果 API 调用完成(返回一些音频)或发生任何错误时,这些事件将被触发。接下来的几行是字符串变量,我们将用作输入参数。我们有一行用于包含我们的访问令牌信息。最后一行创建了一个新列表,我们将用它来存储我们的请求头。
我们向我们的类中添加了两个常量字符串,如下所示:
private const string RequestUri = "https://speech.platform.bing.com/synthesize";
private const string SsmlTemplate =
"<speak version='1.0'xml:lang='en-US'>
<voice xml:lang='en-US'xml:gender='{0}'
name='{1}'>{2}
</voice>
</speak>";
第一个字符串包含请求 URI。这是我们执行请求需要调用的 REST API 端点。接下来,我们有一个定义我们的语音合成标记语言(SSML)模板的字符串。这是我们指定语音服务应该说什么,以及如何说的地方。
接下来,我们创建我们的构造函数,如下所示:
public TextToSpeak()
{
_gender = "Female";
_outputFormat = "riff-16khz-16bit-mono-pcm";
_voiceName = "Microsoft Server Speech Text to Speech Voice (en-US, ZiraRUS)";
}
在这里,我们只是在初始化我们之前声明的某些变量。如您所见,我们正在定义声音为女性,并定义它使用特定的声音。在性别方面,可以是女性或男性。声音名称可以是长列表中的一个选项。我们将在稍后的章节中更详细地了解该列表的细节。
最后一行指定了音频的输出格式。这将定义结果音频流的格式和编解码器。同样,这可以有多种选择,我们将在后面的章节中探讨。
在构造函数之后,我们将创建三个公共方法。这些方法将生成一个认证令牌和一些 HTTP 头信息,并最终执行我们对 API 的调用。在我们创建这些方法之前,你应该添加两个辅助方法来能够引发我们的事件。将它们命名为RaiseOnAudioAvailable和RaiseOnError方法。它们应该接受AudioEventArgs和AudioErrorEventArgs作为参数。
接下来,添加一个名为GenerateHeaders的新方法,如下所示:
public void GenerateHeaders()
{
_headers.Add(new KeyValuePair<string, string>("Content-Type", "application/ssml+xml"));
_headers.Add(new KeyValuePair<string, string>("X-Microsoft-OutputFormat", _outputFormat));
_headers.Add(new KeyValuePair<string, string>("Authorization", _authorizationToken));
_headers.Add(new KeyValuePair<string, string>("X-Search-AppId", Guid.NewGuid().ToString("N")));
_headers.Add(new KeyValuePair<string, string>("X-Search-ClientID", Guid.NewGuid().ToString("N")));
_headers.Add(new KeyValuePair<string, string>("User-Agent", "Chapter1"));
}
在这里,我们将 HTTP 头信息添加到我们之前创建的列表中。这些头信息对于服务响应是必需的,如果缺少任何,它将返回HTTP/400响应。我们将在稍后更详细地介绍我们使用哪些头信息。现在,只需确保它们存在即可。
在此之后,我们想要添加一个名为GenerateAuthenticationToken的新方法,如下所示:
public bool GenerateAuthenticationToken(string clientSecret)
{
Authentication auth = new Authentication(clientSecret);
这个方法接受一个字符串参数,即客户端密钥(你的 API 密钥)。首先,我们创建一个Authentication类的新对象,这是我们之前看过的,如下所示:
try
{
_token = auth.Token;
if (_token != null)
{
_authorizationToken = $"Bearer {_token}";
return true;
}
else
{
RaiseOnError(new AudioErrorEventArgs("Failed to generate authentication token."));
return false;
}
}
我们使用认证对象来检索访问令牌。这个令牌用于我们的授权令牌字符串,正如我们之前看到的,它被包含在我们的头信息中。如果应用程序由于某种原因无法生成访问令牌,我们将触发一个错误事件。
通过添加相关的catch子句来完成这个方法。如果发生任何异常,我们希望引发一个新的错误事件。
在这个类中,我们需要创建的最后一个方法将被命名为SpeakAsync方法,如下面的截图所示。这个方法实际上会向语音 API 发起请求:
public Task SpeakAsync(string textToSpeak, CancellationTokencancellationToken)
{
varcookieContainer = new CookieContainer();
var handler = new HttpClientHandler() {
CookieContainer = cookieContainer
};
var client = new HttpClient(handler);
这个方法接受两个参数。一个是字符串,它将是我们要被读出的文本。下一个是cancellationToken;这可以用来传播给定操作应该被取消的命令。
进入方法时,我们创建了三个我们将用于执行请求的对象。这些是从.NET 库中的类。我们不会对它们进行更详细的介绍。
我们之前生成了一些头信息,我们需要将这些添加到我们的 HTTP 客户端中。我们通过在先前的foreach循环中添加头信息来实现,基本上是遍历整个列表,如下面的代码所示:
foreach(var header in _headers)
{
client.DefaultRequestHeaders.TryAddWithoutValidation (header.Key, header.Value);
}
接下来,我们创建一个HTTP 请求消息,指定请求 URI 以及我们将通过POST方法发送数据的事实。我们还使用我们之前创建的 SSML 模板指定内容,添加正确的参数(性别、语音名称和要被读出的文本),如下面的代码所示:
var request = new HttpRequestMessage(HttpMethod.Post, RequestUri)
{
Content = new StringContent(string.Format(SsmlTemplate, _gender, _voiceName, textToSpeak))
};
我们使用 HTTP 客户端异步发送 HTTP 请求,如下所示:
var httpTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
以下代码是之前我们进行的异步发送调用的延续。这将异步运行,并检查响应的状态。如果响应成功,它将作为流读取响应消息并触发音频事件。如果一切顺利,那么该流应该包含我们用语音说的文本:
var saveTask = httpTask.ContinueWith(async (responseMessage, token) =>
{
try
{
if (responseMessage.IsCompleted &&
responseMessage.Result != null &&
responseMessage.Result.IsSuccessStatusCode) {
var httpStream = await responseMessage. Result.Content.ReadAsStreamAsync().ConfigureAwait(false);
RaiseOnAudioAvailable(new AudioEventArgs (httpStream));
} else {
RaiseOnError(new AudioErrorEventArgs($"Service returned {responseMessage.Result.StatusCode}"));
}
}
catch(Exception e)
{
RaiseOnError(new AudioErrorEventArgs (e.GetBaseException().Message));
}
}
如果响应指示的不是成功,我们将引发错误事件。
我们还希望为此添加一个catch子句和一个finally子句。如果在finally子句中捕获到异常,则引发错误并销毁所有使用的对象。
我们需要的最终代码指定了延续任务附加到父任务上。我们还需要向此任务添加cancellationToken。添加以下代码来完成方法:
}, TaskContinuationOptions.AttachedToParent, cancellationToken);
return saveTask;
}
在此基础上,我们现在可以在我们的应用程序中利用这个类。打开MainViewModel.cs文件,并声明一个新的类变量,如下所示:
private TextToSpeak _textToSpeak;
在构造函数中添加以下代码以初始化新添加的对象。我们还需要调用一个函数来生成认证令牌,如下所示:
_textToSpeak = new TextToSpeak();
_textToSpeak.OnAudioAvailable += _textToSpeak_OnAudioAvailable;
_textToSpeak.OnError += _textToSpeak_OnError;
GenerateToken();
在我们创建了对象之后,我们将两个事件连接到事件处理器上。然后,我们通过创建一个包含以下内容的GenerateToken函数来生成一个认证令牌:
public async void GenerateToken()
{
if (await _textToSpeak.GenerateAuthenticationToken("BING_SPEECH_API_KEY_HERE"))
_textToSpeak.GenerateHeaders();
}
然后,我们生成一个认证令牌,指定必应语音 API 的 API 密钥。如果该调用成功,我们生成所需的 HTTP 头。
我们还需要添加事件处理器,因此首先创建_textToSpeak_OnError方法,如下所示:
private void _textToSpeak_OnError(object sender, AudioErrorEventArgs e)
{
StatusText = $"Status: Audio service failed - {e.ErrorMessage}";
}
这应该是一个相当简单的方法,只需将错误消息输出到用户的状态文本字段即可。
接下来,我们需要创建一个_textToSpeak_OnAudioAvailable方法,如下所示:
private void _textToSpeak_OnAudioAvailable(object sender, AudioEventArgs e)
{
SoundPlayer player = new SoundPlayer(e.EventData);
player.Play();
e.EventData.Dispose();
}
在这里,我们利用.NET 框架中的SoundPlayer类。这允许我们直接添加流数据并简单地播放消息。
为了让一切正常工作,我们需要调用SpeakAsync方法。我们可以在DetectFace方法的末尾添加以下内容来实现这一点:
await _textToSpeak.SpeakAsync(textToSpeak, CancellationToken.None);
在此基础上,你现在应该能够编译并运行应用程序。通过加载一张照片并点击检测面部,你应该能够听到图像中面部数量的语音反馈。只需记得打开你的音频即可!
摘要
本章简要介绍了微软认知服务。我们首先创建了一个模板项目,以便为即将到来的章节轻松创建新项目。我们通过为本章创建一个示例项目来尝试这个模板。然后,你学习了如何通过利用面部 API 在图像中检测面部。从那里,我们快速浏览了认知服务提供的内容。最后,我们通过使用必应语音 API 为我们的应用程序添加了文本到语音的功能。
下一章将详细介绍 API 的视觉部分。在那里,你将学习如何使用计算机视觉 API 分析图像。你将更深入地了解面部 API,并学习如何通过使用情感 API 来检测面部表情。我们将利用这些知识开始构建我们的智能家居应用程序。
第二章。分析图像以识别面部
"我们可以使用计算机视觉 API 向我们的客户证明数据的可靠性,这样他们就可以基于这些信息做出重要的商业决策,并对此充满信心。"
- Leendert de Voogd,Vigiglobe 首席执行官
在上一章中,您简要介绍了 Microsoft Cognitive Services。在本章中,我们将深入探讨视觉 API 中的基于图像的 API。我们将学习如何执行图像分析。接下来,我们将更深入地研究 Face API,这是我们上一章简要了解的 API,我们将学习如何识别人。然后,我们将学习如何使用 Face API 在面部中识别情绪。最后,我们将了解不同的内容审核方式。
在本章中,我们将涵盖以下主题:
-
分析图像以识别内容、元数据和成人评级。
-
在图像中识别名人以及在图像中读取文本。
-
深入 Face API:
-
学习找到两个面部属于同一个人的可能性
-
根据视觉相似性分组面部并搜索相似面部
-
从面部识别一个人
-
识别情绪
-
-
内容审核。
使用计算机视觉 API 分析图像
计算机视觉 API 允许我们处理图像并检索有关它的信息。它依赖于先进的算法,根据我们的需求以不同的方式分析图像内容。
在本节中,我们将学习如何利用这个 API。我们将通过独立的示例来查看分析图像的不同方法。我们将在后面的章节中将一些我们将要介绍的功能整合到我们的端到端应用中。
调用任何 API 将返回以下响应代码之一:
| 代码 | 描述 |
|---|---|
200 |
以 JSON 格式提取的特征信息。 |
400 |
通常这意味着请求错误。可能是一个无效的图像 URL、过小或过大的图像、无效的图像格式或与请求体相关的任何其他错误。 |
415 |
不支持的媒体类型。 |
500 |
可能的错误包括处理图像失败、图像处理超时或内部服务器错误。 |
设置章节示例项目
在我们进入 API 的细节之前,我们需要为本章创建一个示例项目。该项目将包含所有示例,这些示例在此阶段不会放入端到端应用中:
注意
如果您还没有这样做,请访问portal.azure.com注册计算机视觉 API 密钥。
-
使用我们在第一章中创建的模板在 Visual Studio 中创建一个新的项目,使用 Microsoft Cognitive Services 入门。
-
右键单击项目并选择管理 NuGet 包。搜索
Microsoft.ProjectOxford.Vision包并将其安装到项目中,如下面的截图所示:![设置章节示例项目]()
-
创建以下
UserControls文件,并将它们添加到ViewModel文件夹中:-
CelebrityView.xaml -
DescriptionView.xaml -
ImageAnalysisView.xaml -
OcrView.xaml -
ThumbnailView.xaml
-
-
还要将以下列表中的相应
ViewModel实例添加到ViewModel文件夹中:-
CelebrityViewModel.cs -
DescriptionViewModel.cs -
ImageAnalysisViewModel.cs -
OcrViewModel.cs -
ThumbnailViewModel.cs
-
检查新创建的ViewModel实例,确保所有类都是公开的。
我们将使用TabControl标签在不同的视图之间切换。打开MainView.xaml文件,并在预先创建的Grid标签中添加以下内容:
<TabControl x: Name = "tabControl"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "810" Height = "520">
<TabItem Header="Analysis" Width="100">
<controls:ImageAnalysisView />
</TabItem>
<TabItem Header="Description" Width="100">
<controls:DescriptionView />
</TabItem>
<TabItem Header="Celebs" Width="100">
<controls:CelebrityView />
</TabItem>
<TabItem Header="OCR" Width="100">
<controls:OcrView />
</TabItem>
<TabItem Header="Thumbnail" Width="100">
<controls:ThumbnailView />
</TabItem>
</TabControl>
这将在应用程序顶部添加一个标签栏,允许您在不同视图之间导航。
接下来,我们将添加MainViewModel.cs文件中所需的属性和成员。
以下变量用于访问计算机视觉 API:
private IVisionServiceClient _visionClient;
以下代码声明了一个包含CelebrityViewModel对象的私有变量。它还声明了public属性,我们使用它来在我们的View中访问ViewModel:
private CelebrityViewModel _celebrityVm;
public CelebrityViewModel CelebrityVm
{
get { return _celebrityVm; }
set
{
_celebrityVm = value;
RaisePropertyChangedEvent("CelebrityVm");
}
}
按照相同的模式,为创建的其余ViewModel实例添加属性。
在所有属性就绪后,使用以下代码在我们的构造函数中创建ViewModel实例:
public MainViewModel()
{
_visionClient = new VisionServiceClient("VISION_API_KEY_HERE", "ROOT_URI");
CelebrityVm = new CelebrityViewModel(_visionClient);
DescriptionVm = new DescriptionViewModel(_visionClient);
ImageAnalysisVm= new ImageAnalysisViewModel(_visionClient);
OcrVm = new OcrViewModel(_visionClient);
ThumbnailVm = new ThumbnailViewModel(_visionClient);
}
注意我们首先使用之前注册的 API 密钥和根 URI 创建VisionServiceClient对象,如第一章中所述,使用 Microsoft 认知服务入门。然后将其注入到所有ViewModel实例中供使用。
现在应该可以编译,并显示以下截图所示的应用程序:

通用图像分析
我们通过向ImageAnalysis.xaml文件添加 UI 来开始启用通用图像分析。所有计算机视觉示例 UI 将以相同的方式构建。
UI 应该有两个列,如下面的代码所示:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
第一个将包含图像选择,而第二个将显示我们的结果。
在左侧列中,我们创建一个垂直方向的StackPanel标签。为此,我们添加一个标签和一个ListBox标签。列表框将显示我们可以添加到我们的分析查询中的视觉特征列表。注意以下代码中ListBox标签中连接的SelectionChanged事件。这将在代码之后添加,并将在稍后介绍:
<StackPanel Orientation="Vertical"Grid.Column="0">
<TextBlock Text="Visual Features:"
FontWeight="Bold"
FontSize="15"
Margin="5, 5" Height="20" />
<ListBox: Name = "VisualFeatures"
ItemsSource = "{Binding ImageAnalysisVm.Features}"
SelectionMode = "Multiple" Height="150" Margin="5, 0, 5, 0"
SelectionChanged = "VisualFeatures_SelectionChanged" />
列表框将能够选择多个项目,并将项目收集在ViewModel中。
在同一个堆叠面板中,我们还添加了一个按钮元素和一个图像元素。这些将允许我们浏览图像、显示它并分析它。Button命令和图像源都绑定到ViewModel中的相应属性,如下所示:
<Button Content = "Browse and analyze"
Command = "{Binding ImageAnalysisVm.BrowseAndAnalyzeImageCommand}"
Margin="5, 10, 5, 10" Height="20" Width="120"
HorizontalAlignment="Right" />
<Image Stretch = "Uniform"
Source="{Binding ImageAnalysisVm.ImageSource}"
Height="280" Width="395" />
</StackPanel>
我们还添加了另一个垂直堆叠面板。这将放置在右侧列。它包含一个标题标签以及一个文本框,绑定到我们的ViewModel中的分析结果,如下所示:
<StackPanel Orientation= "Vertical"Grid.Column="1">
<TextBlock Text="Analysis Results:"
FontWeight = "Bold"
FontSize="15" Margin="5, 5" Height="20" />
<TextBox Text = "{Binding ImageAnalysisVm.AnalysisResult}"
Margin="5, 0, 5, 5" Height="485" />
</StackPanel>
接下来,我们想要将我们的SelectionChanged事件处理程序添加到我们的代码隐藏中。打开ImageAnalysisView.xaml.cs文件并添加以下内容:
private void VisualFeatures_SelectionChanged(object sender, SelectionChangedEventArgs e) {
var vm = (MainViewModel) DataContext;
vm.ImageAnalysisVm.SelectedFeatures.Clear();
函数的第一行将给我们当前的DataContext,即MainViewModel类。我们访问ImageAnalysisVm属性,即我们的ViewModel,并清除选中的视觉特征列表。
从那里,我们遍历我们的列表框中的选中项。所有项都将添加到我们的ViewModel中的SelectedFeatures列表中:
foreach(VisualFeature feature in VisualFeatures.SelectedItems)
{
vm.ImageAnalysisVm.SelectedFeatures.Add(feature);
}
}
打开ImageAnalysisViewModel.cs文件。确保该类继承自ObservableObject类。
声明一个private变量,如下所示:
private IVisionServiceClient _visionClient;
这将用于访问计算机视觉 API,并通过构造函数进行初始化。
接下来,我们声明一个私有变量及其对应属性,用于我们的视觉特征列表,如下所示:
private List<VisualFeature> _features=new List<VisualFeature>();
public List<VisualFeature> Features {
get { return _features; }
set {
_features = value;
RaisePropertyChangedEvent("Features");
}
}
以类似的方式,创建一个名为ImageSource的BitmapImage变量和属性。创建一个名为SelectedFeatures的VisualFeature类型列表和一个名为AnalysisResult的字符串。
我们还需要声明我们的按钮属性,如下所示:
public ICommandBrowseAndAnalyzeImageCommand {get; private set;}
在此基础上,我们创建我们的构造函数,如下所示:
public ImageAnalysisViewModel(IVisionServiceClientvisionClient) {
_visionClient = visionClient;
Initialize();
}
构造函数接受一个参数,即我们在MainViewModel文件中创建的IVisionServiceClient对象。它将该参数分配给之前创建的变量。然后我们调用一个Initialize函数,如下所示:
private void Initialize() {
Features = Enum.GetValues(typeof(VisualFeature))
.Cast<VisualFeature>().ToList();
BrowseAndAnalyzeImageCommand = new DelegateCommand(BrowseAndAnalyze);
}
在Initialize函数中,我们从enum类型的VisualFeature变量中获取所有值。这些值被添加到特征列表中,该列表在 UI 中显示。我们还创建了我们的按钮,现在我们已经这样做,我们需要创建相应的操作,如下所示:
private async void BrowseAndAnalyze(object obj)
{
var openDialog = new Microsoft.Win32.OpenFileDialog();
openDialog.Filter = "JPEG Image(*.jpg)|*.jpg";
bool? result = openDialog.ShowDialog();
if (!(bool)result) return;
string filePath = openDialog.FileName;
Uri fileUri = new Uri(filePath);
BitmapImage image = new BitmapImage(fileUri);
image.CacheOption = BitmapCacheOption.None;
image.UriSource = fileUri;
ImageSource = image;
上述代码的前几行与我们在第一章中做的类似,使用 Microsoft 认知服务入门。我们打开文件浏览器并获取选中的图像。
在选中图像后,我们按照以下方式对其进行分析:
try {
using (StreamfileStream = File.OpenRead(filePath)) {
AnalysisResult analysisResult = await _visionClient.AnalyzeImageAsync(fileStream, SelectedFeatures);
我们调用我们的_visionClient的AnalyzeImageAsync函数。这个函数有四个重载,它们都非常相似。在我们的情况下,我们传递一个Stream类型的图像和包含VisualFeatures变量的SelectedFeatures列表来分析。
请求参数如下:
| 参数 | 描述 |
|---|---|
| 图像(必需) |
-
可以以原始图像二进制或 URL 的形式上传。
-
可以是 JPEG、PNG、GIF 或 BMP。
-
文件大小必须小于 4 MB。
-
图像尺寸必须至少为 50 x 50 像素。
|
| 可选视觉特征 | 一个指示要返回的视觉特征类型的列表。它可以包括类别、标签、描述、面部、图像类型、颜色以及是否为成人内容。 |
|---|---|
| 详细信息(可选) | 一个指示要返回的特定领域详细信息的列表。 |
对此请求的响应是AnalysisResult字符串。
我们随后检查结果是否为null。如果不是,我们调用一个函数来解析它,并将结果分配给我们的AnalysisResult字符串,如下所示:
if (analysisResult != null)
AnalysisResult = PrintAnalysisResult(analysisResult);
记得关闭try子句,并用相应的catch子句完成方法。
AnalysisResult字符串包含根据 API 调用中请求的视觉特征的数据。
AnalysisResult变量中的数据在以下表中描述:
| 视觉特征 | 描述 |
|---|---|
| 类别 | 图像根据定义的分类法进行分类。这包括从动物、建筑和户外到人物的各个方面。 |
| 标签 | 图像用与内容相关的单词列表进行标记。 |
| 描述 | 这包含一个完整句子来描述图像。 |
| 面部 | 这检测图像中的面部,并包含面部坐标、性别和年龄。 |
| 图像类型 | 这检测图像是剪贴画还是线画。 |
| 颜色 | 这包含有关主导颜色、强调颜色以及图像是否为黑白的详细信息。 |
| 成人 | 这检测图像是否具有色情性质,以及是否过于露骨。 |
要检索数据,例如类别,你可以使用以下方法:
if (analysisResult.Description != null) {
result.AppendFormat("Description: {0}\n", analysisResult.Description.Captions[0].Text);
result.AppendFormat("Probability: {0}\n\n", analysisResult.Description.Captions[0].Confidence);
}
成功的调用将显示以下结果:

有时,你可能只对图像描述感兴趣。在这种情况下,请求我们刚刚所做的全面分析是浪费的。通过调用以下函数,你将得到一个描述数组:
AnalysisResultdescriptionResult = await _visionClient.DescribeAsync(ImageUrl, NumberOfDescriptions);
在这次调用中,我们指定了图像的 URL 和要返回的描述数量。第一个参数必须始终包含,但它可以是图像上传而不是 URL。第二个参数是可选的,如果没有提供,则默认为 1。
成功的查询将导致一个AnalysisResult对象,这与前面代码中描述的是同一个。在这种情况下,它将只包含请求 ID、图像元数据和标题数组。每个标题包含一个图像描述和该描述正确性的置信度。
我们将在后面的章节中将这种图像分析方法添加到我们的智能家居应用程序中。
使用领域模型识别名人
计算机视觉 API 的一个特点是能够识别特定领域的内容。在撰写本文时,该 API 仅支持名人识别,能够识别大约 20 万名名人。
对于这个示例,我们选择使用互联网上的图片。然后 UI 需要一个文本框来输入 URL。它需要一个按钮来加载图片并执行领域分析。应该有一个图片元素来查看图片,以及一个文本框来输出结果。
相应的ViewModel应该有两个string属性,用于 URL 和分析结果。它应该有一个BitmapImage属性用于图片,以及一个ICommand属性用于我们的按钮。
在ViewModel的开始处添加一个private变量用于IVisionServiceClient类型,如下所示:
private IVisionServiceClient _visionClient;
这应该在构造函数中分配,该构造函数将接受IVisionServiceClient类型的参数。
由于我们需要一个 URL 从互联网上获取图片,我们需要使用动作和谓词初始化Icommand属性。后者检查 URL 属性是否已设置,如下面的代码所示:
public CelebrityViewModel(IVisionServiceClient visionClient) {
_visionClient = visionClient;
LoadAndFindCelebrityCommand = new DelegateCommand(LoadAndFindCelebrity, CanFindCelebrity);
}
LoadAndFindCelebrity加载操作使用给定的 URL 创建一个Uri。使用这个 URL,它创建一个BitmapImage并将其分配给ImageSource属性,如下面的代码所示。图片应该在 UI 中可见:
private async void LoadAndFindCelebrity(object obj) {
UrifileUri = new Uri(ImageUrl);
BitmapImage image = new BitmapImage(fileUri);
image.CacheOption = BitmapCacheOption.None;
image.UriSource = fileUri;
ImageSource = image;
我们使用给定的 URL 调用AnalyzeImageInDomainAsync类型,如下面的代码所示。我们传递的第一个参数是图片 URL。或者,这也可以是一个已作为Stream类型打开的图片:
try {
AnalysisInDomainResultcelebrityResult = await _visionClient.AnalyzeImageInDomainAsync(ImageUrl, "celebrities");
if (celebrityResult != null)
Celebrity = celebrityResult.Result.ToString();
}
第二个参数是领域模型名称,它以string格式存在。作为替代,我们本可以使用特定的Model对象,这可以通过调用以下代码来检索:
VisionClient.ListModelsAsync();
这将返回一个Models数组,我们可以从中显示和选择。由于目前只有一个可用,这样做没有意义。
AnalyzeImageInDomainAsync的结果是AnalysisInDomainResult类型的对象。该对象将包含请求 ID、图片元数据以及包含名人数组的结果。在我们的例子中,我们简单地输出整个结果数组。数组中的每个项目将包含名人的名字、匹配的置信度以及图像中的脸矩形。请尝试在提供的示例代码中尝试。
利用光学字符识别
对于某些任务,光学字符识别(OCR)可能非常有用。比如说,你拍了一张收据的照片。使用 OCR,你可以从照片本身读取金额,并自动添加到会计中。
OCR 将检测图片中的文本并提取可读字符。它将自动检测语言。可选地,API 将检测图片方向并在读取文本之前进行校正。
要指定语言,您需要使用BCP-47语言代码。在撰写本文时,以下语言受到支持:简体中文、繁体中文、捷克语、丹麦语、荷兰语、英语、芬兰语、法语、德语、希腊语、匈牙利语、意大利语、日语、韩语、挪威语、波兰语、葡萄牙语、俄语、西班牙语、瑞典语、土耳其语、阿拉伯语、罗马尼亚语、西里尔塞尔维亚语、拉丁塞尔维亚语和斯洛伐克语。
在代码示例中,UI 将有一个图像元素。它还将有一个按钮来加载图像并检测文本。结果将打印到文本框元素中。
ViewModel需要一个用于结果的string属性,一个用于图像的BitmapImage属性,以及一个用于按钮的ICommand属性。
为计算机视觉 API 在ViewModel中添加一个private变量,如下所示:
private IVisionServiceClient _visionClient;
构造函数应有一个IVisionServiceClient类型的参数,该参数应分配给前面的变量。
为我们的按钮创建一个作为命令的函数。命名为BrowseAndAnalyze,并接受object作为参数。然后打开文件浏览器并找到一个要分析的图像。选择图像后,我们运行 OCR 分析,如下所示:
using (StreamfileStream = File.OpenRead(filePath)) {
OcrResultsanalysisResult = await _visionClient.RecognizeTextAsync (fileStream);
if(analysisResult != null)
OcrResult = PrintOcrResult(analysisResult);
}
将图像以Stream类型打开,我们调用RecognizeTextAsync方法。在这种情况下,我们传递图像作为Stream类型,但我们也可以简单地传递图像的 URL。
在此调用中可以指定两个更多参数。首先,您可以指定文本的语言。默认为未知,这意味着 API 将尝试自动检测语言。其次,您可以指定 API 是否应检测图像的方向。默认设置为false。
如果调用成功,它将以OcrResults对象的形式返回数据。我们将此结果发送到PrintOcrResult函数,我们将解析它并打印文本,如下所示:
private string PrintOcrResult(OcrResultsocrResult)
{
StringBuilder result = new StringBuilder();
result.AppendFormat("Language is {0}\n", ocrResult.Language);
result.Append("The words are:\n\n");
首先,我们创建一个StringBuilder对象,它将保存所有文本。我们首先添加到其中的内容是图像中文本的语言,如下所示:
foreach(var region in ocrResult.Regions) {
foreach(var line in region.Lines) {
foreach(var text in line.Words) {
result.AppendFormat("{0} ", text.Text);
}
result.Append("\n");
}
result.Append("\n\n");
}
结果包含一个数组,该数组包含Regions属性。每个项目代表识别出的文本,每个区域包含多行。line变量是数组,其中每个项目代表识别出的文本。每一行包含Words属性的数组。该数组中的每个项目代表一个识别出的单词。
将所有单词附加到StringBuilder函数后,我们将其作为字符串返回。然后,它将在 UI 中打印出来,如下面的截图所示:

结果还包含文本的方向和角度。结合也包括的边界框,你可以在原始图像中标记每个单词。
生成图像缩略图
在当今世界,作为开发者,我们在显示图片时必须考虑不同的屏幕尺寸。计算机视觉 API 通过提供生成缩略图的能力来提供一些帮助。
缩略图生成本身并不是什么大事。使 API 变得聪明的是,它会分析图像并确定感兴趣的区域。
它还将生成智能裁剪坐标。这意味着如果指定的宽高比与原始图像不同,它将裁剪图像,重点在感兴趣的区域。
在示例代码中,UI 由两个图像元素和一个按钮组成。第一个图像是原始大小的图像。第二个是生成的缩略图,我们指定其大小为 250 x 250 像素。
View模型将需要相应的属性,两个BitmapImages方法作为图像源,以及一个ICommand属性用于我们的按钮命令。
在ViewModel中定义一个私有变量,如下所示:
private IVisionServiceClient _visionClient;
这将是我们的 API 访问点。构造函数应该接受一个IVisionServiceClient对象,该对象应分配给前面的变量。
对于ICommand属性,我们创建一个函数BrowseAndAnalyze,接受一个object参数。我们不需要检查我们是否可以执行该命令。我们将每次都浏览一个图像。
在BrowseAndAnalyze函数中,我们打开文件对话框并选择一个图像。当我们有了图像文件路径,我们可以生成我们的缩略图,如下所示:
using (StreamfileStream = File.OpenRead(filePath))
{
byte[] thumbnailResult = await _visionClient.GetThumbnailAsync(fileStream, 250, 250);
if(thumbnailResult != null &&thumbnailResult.Length != 0)
CreateThumbnail(thumbnailResult);
}
我们打开图像文件,以便我们有一个Stream类型。这个流是我们调用GetThumbnailAsync方法的第一个参数。接下来的两个参数表示我们想要的缩略图的宽度和高度。
默认情况下,API 调用将使用智能裁剪,因此我们无需指定。如果我们有不需要智能裁剪的情况,我们可以添加一个bool变量作为第四个参数。
如果调用成功,我们将返回一个byte数组。这是图像数据。如果它包含数据,我们将将其传递给一个新的函数CreateThumbnail,从中创建一个BitmapImage对象,如下所示:
private void CreateThumbnail(byte[] thumbnailResult)
{
try {
MemoryStreamms = new MemoryStream(thumbnailResult);
ms.Seek(0, SeekOrigin.Begin);
要从byte数组创建图像,我们从这个数组创建一个MemoryStream对象。我们确保从数组的开始处开始。
接下来,我们创建一个BitmapImage对象并开始初始化它。我们指定CacheOption并将StreamSource设置为之前创建的MemoryStream变量。最后,我们停止BitmapImage的初始化并将图像分配给我们的Thumbnail属性,如下所示:
BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.None;
image.StreamSource = ms;
image.EndInit();
Thumbnail = image;
收尾try块并添加相应的catch块。你现在应该能够生成缩略图。
深入了解 Face API
Face API 有两个主要功能。第一个是面部检测,另一个是面部识别。
面部检测使我们能够在一张图像中检测多达 64 个面部。我们已经看到了基本用法。面部识别的特点隐含在其名称中:使用它,我们可以检测两个面部是否属于同一个人。我们可以找到相似的面部,或者特定的一个,并且我们可以将相似的面部分组。我们将在以下章节中学习如何做到这一切。
当调用任何 API 时,它将响应以下之一:
| 代码 | 描述 |
|---|---|
200 |
调用成功。它返回一个包含与 API 调用相关的数据的数组。 |
400 |
请求体无效。这可能是 API 调用中的多种错误,通常请求代码无效。 |
401 |
访问被拒绝,因为订阅密钥无效。密钥可能错误,或者账户/订阅计划可能已被阻止。 |
403 |
超出调用量数据。您已使用本月的所有可用 API 调用。 |
415 |
无效的媒体类型。 |
429 |
超出速率限制。您需要等待一段时间(免费预览中少于一分钟)后再尝试。 |
从检测到的面部获取更多信息
在 第一章 使用 Microsoft Cognitive Services 入门 中,我们学习了面部检测的基本形式。在示例中,我们检索了一个 Face 数组。它包含了图像中找到的所有面部信息。在特定示例中,我们获得了关于面部矩形、面部 ID、面部特征点和年龄的信息。
在调用 API 时,有四个请求参数,如下表所示:
| 参数 | 描述 |
|---|---|
image |
-
在其中搜索面部的图像。它可以是 URL 或二进制数据的形式。
-
支持的格式为 JPEG、PNG、GIF 和 BMP。
-
最大文件大小为 4 MB。
-
可检测到的面部大小介于 36 x 36 像素和 4096 x 4096 像素之间。
|
return FaceId (optional) |
布尔值。此值指定响应是否应包含面部 ID。 |
|---|---|
return FaceLandmarks (optional) |
布尔值。此值指定响应是否应包含检测到的面部 FaceLandmarks。 |
return FaceAttributes (optional) |
-
字符串值。这是一个逗号分隔的字符串,包含要分析的所有面部属性。
-
支持的属性包括年龄、性别、头部姿态、微笑、面部毛发、情绪和眼镜。
-
这些属性仍然是实验性的,应如此对待。
|
如果成功发现面部,它将在 24 小时后过期。在调用 Face API 的其他部分时,通常需要面部 ID 作为输入。在这些情况下,我们需要首先检测面部,然后使用检测到的面部作为参数调用我们希望使用的 API。
使用这些知识,我挑战你尝试在 第一章 使用 Microsoft Cognitive Services 入门 中的示例。在面部周围画一个矩形。在图像中标记眼睛。
判断两个面部是否属于同一个人
为了决定两个面部是否属于同一个人,我们将调用 API 的Verify函数。API 允许我们检测两个面部是否属于同一个人,这被称为面对面验证。检测一个面部是否属于特定的人被称为人脸对人物验证。
UI 将包括三个按钮元素、两个图像元素和一个文本块元素。其中两个按钮将用于浏览图像,然后显示在每个图像元素中。最后一个按钮将执行验证。文本块将输出结果。
按照你想要的布局来布局 UI,并将不同的元素绑定到ViewModel中的属性,就像我们之前做的那样。在ViewModel中,应该有两个BitmapImage属性用于图像元素。应该有一个string属性,包含验证结果。最后,应该有三个ICommand属性,每个按钮一个。
请记住将 UI 添加到MainView.xaml文件中作为一个新的TabItem。此外,将ViewModel添加到MainViewModel.cs文件中,在那里你还需要为FaceServiceClient变量添加一个新的变量。这个变量应该使用我们在第一章中注册的 Face API 密钥创建,即使用 Microsoft 认知服务入门。
在ViewModel中,我们需要声明以下三个private变量:
private FaceServiceClient _faceServiceClient;
private Guid _faceId1 = Guid.Empty;
private Guid _faceId2 = Guid.Empty;
我们之前已经看到过第一个;它将访问 Face API。当进行人脸检测时,将分配两个Guid变量。
构造函数接受一个参数,即我们的FaceServiceClient对象。这个对象被分配给之前创建的变量,如下面的代码所示:
public FaceVerificationViewModel (FaceServiceClientfaceServiceClient)
{
_faceServiceClient = faceServiceClient;
Initialize();
}
从构造函数中,我们调用Initialize函数来创建DelegateCommand属性,如下所示:
private void Initialize()
{
BrowseImage1Command = new DelegateCommand(BrowseImage1);
BrowseImage2Command = new DelegateCommand(BrowseImage2);
VerifyImageCommand = new DelegateCommand(VerifyFace, CanVerifyFace);
}
浏览命令在任何时候都不需要被禁用,所以我们只需传递命令函数,如下所示:
private async void BrowseImage1(object obj) {
Image1Source = await BrowseImageAsync(1);
}
两个函数看起来很相似。我们调用另一个函数来浏览图像并检测人脸。为了分隔每个图像,我们传递图像编号。
BrowseImageAsync函数将接受一个int类型的参数。它返回一个BitmapImage对象,我们将它分配给绑定到我们 UI 的BitmapImage属性。第一部分打开浏览对话框并返回选定的图像。当我们有了图像及其路径时,我们将跳入。
我们将图像作为Stream对象打开。Stream对象用于 API 调用以检测人脸。当我们调用 API 时,我们可以使用默认调用,因为它将返回我们感兴趣的价值,如下面的代码所示:
try {
using (Stream fileStream = File.OpenRead(filePath)) {
Face[] detectedFaces = await _faceServiceClient.DetectAsync(fileStream);
当检测过程完成后,我们检查这是哪个图像,并使用以下代码将FaceId参数分配给正确的Guid变量。对于这个例子,我们假设每个图像中只有一个面部:
if (imagenumber == 1)
_faceId1 = detectedFaces[0].FaceId;
else
_faceId2 = detectedFaces[0].FaceId;
}
}
通过添加适当的捕获子句来完成函数。您还需要从所选图像创建并返回一个BitmapImage参数。
在启用人脸验证按钮之前,我们使用以下代码进行检查,以查看是否已设置两个人脸 ID:
private bool CanVerifyFace(object obj)
{
return !_faceId1.Equals(Guid.Empty) &&! _faceId2.Equals(Guid.Empty);
}
VerifyFace函数并不复杂,如下所示代码所示:
private async void VerifyFace(object obj) {
try {
VerifyResultverificationResult = await _faceServiceClient.VerifyAsync(_faceId1, _faceId2);
设置好人脸 ID 后,我们可以调用 API 的VerifyAsync函数。我们将人脸 ID 作为参数传递,并返回一个VerifyResult对象。我们使用此对象提供输出,如下所示:
FaceVerificationResult = $"The two provided faces is identical: {verificationResult.IsIdentical}, with confidence: {verificationResult.Confidence}";
}
成功调用将返回代码200响应。响应数据是一个bool类型的变量isIdentical和一个数字confidence:

在编写本文时,人脸 API 的NuGet包仅允许进行人脸对人脸验证。如果我们直接调用 REST API,我们还可以利用人脸对人员验证。
要使用人脸对人员验证,只需要一张图像。您需要传递该图像的人脸 ID。您还需要传递人员组 ID 和人员 ID。这些是为了指定要搜索的特定人员组以及该组中的特定人员。我们将在本章后面介绍人员组和人员。
寻找相似人脸
使用人脸 API,您可以找到与提供的脸相似的人脸。API 允许两种搜索模式。匹配人员模式是默认模式。这将根据内部相同人员的阈值将人脸与同一人匹配。另一种是匹配人脸模式,它将忽略相同人员的阈值。这返回相似度匹配,但相似度可能较低。
在提供的示例代码中,我们的 UI 中有三个按钮:一个用于生成人脸列表,另一个用于将人脸添加到列表中,最后一个是用于查找相似人脸。我们需要一个文本框来指定人脸列表的名称。为了方便,我们添加了一个列表框,输出从人脸列表中持久化的脸 ID。我们还添加了一个图像元素来显示我们正在检查的图像,以及一个输出结果的文本框。
在相应的ViewModel中,我们需要为图像元素添加一个BitmapImage属性。我们需要两个string属性:一个用于我们的脸列表名称,另一个用于 API 调用结果。为了将数据传递到我们的列表框,我们需要一个包含Guids的ObservableCollection属性。按钮需要连接到单个ICommand属性。
在ViewModel的开始处,我们声明了两个private变量,如下所示代码所示。第一个是一个bool变量,用于指示人脸列表是否已存在。另一个用于访问人脸 API:
private bool _faceListExists = false;
private FaceServiceClient _faceServiceClient;
构造函数应接受FaceServiceClient参数,并将其分配给前面的变量。然后,它将调用一个Initialize函数,如下所示:
private async void Initialize()
{
FaceListName = "Chapter2";
CreateFaceListCommand = new DelegateCommand(CreateFaceListAsync, CanCreateFaceList);
FindSimilarFaceCommand = new DelegateCommand(FindSimilarFace);
AddExampleFacesToListCommand = new DelegateCommand(AddExampleFacesToList, CanAddExampleFaces);
首先,我们将FaceListName属性初始化为Chapter2。然后,我们创建命令对象,指定动作和谓词。
我们通过调用两个函数来完成Initialize函数,如下所示。一个检查人脸列表是否存在,而另一个更新人脸 ID 列表:
await DoesFaceListExistAsync();
UpdateFaceGuidsAsync();
}
要检查给定的人脸列表是否存在,我们首先需要获取所有人脸列表的列表。我们通过调用ListFaceListsAsync方法来完成此操作,它将返回一个FaceListMetadata数组。在遍历数组之前,我们确保结果有数据,如下所示:
private async Task DoesFaceListExistAsync()
{
FaceListMetadata[] faceLists = await _faceServiceClient.ListFaceListsAsync();
从结果数组中,每个FaceListMetadata数组都包含一个人脸列表 ID、人脸列表名称和用户提供的资料。对于本例,我们只对名称感兴趣。如果指定的人脸列表名称是返回的任何人脸列表的名称,我们将_faceListExists参数设置为true,如下所示:
foreach (FaceListMetadatafaceList in faceLists) {
if (faceList.Name.Equals(FaceListName)) {
_faceListExists = true;
break;
}
}
如果人脸列表存在,我们可以更新人脸 ID 列表。
要获取人脸列表中的面孔,我们首先需要获取人脸列表。这通过调用人脸 API 的GetFaceListAsync方法来完成。这需要将人脸列表 ID 作为参数传递。人脸列表 ID 需要是小写字母或数字,且最多可包含 64 个字符。为了简化,我们使用人脸列表名称作为人脸 ID,如下所示:
private async void UpdateFaceGuidsAsync() {
if (!_faceListExists) return;
try {
FaceListfaceList = await _faceServiceClient.GetFaceListAsync(FaceListName.ToLower());
此 API 调用的结果是包含人脸列表 ID 和人脸列表名称的FaceList对象。它还包含用户提供的资料和持久化面孔的数组。
我们检查是否有数据,然后获取持久化面孔的数组。通过遍历这个数组,我们能够获取每个项目的PersistedFaceId参数(作为一个guid变量)和用户提供的资料。持久化的人脸 ID 被添加到FaceIds ObservableCollection中,如下所示:
if (faceList == null) return;
PersonFace[] faces = faceList.PersistedFaces;
foreach (PersonFace face in faces) {
FaceIds.Add(face.PersistedFaceId);
}
通过添加相应的catch子句来完成函数。
如果人脸列表不存在且我们指定了人脸列表名称,则可以创建一个新的人脸列表,如下所示:
private async void CreateFaceListAsync(object obj) {
try {
if (!_faceListExists) {
await _faceServiceClient.CreateFaceListAsync (
FaceListName.ToLower(), FaceListName, string.Empty);
await DoesFaceListExistAsync();
}
}
首先,我们检查人脸列表是否存在。使用_faceServiceClient参数,您需要传递人脸列表 ID、人脸列表名称和用户数据。如前所述,人脸列表 ID 需要是小写字母或数字。
使用 REST API 时,用户参数是可选的,因此您不必提供它。
在我们创建人脸列表后,我们想要确保它存在。我们通过调用之前创建的DoesFaceListExistAsync函数来完成此操作。添加catch子句以完成函数。
如果命名的人脸列表存在,我们可以向此列表添加面孔。添加AddExampleFacesToList函数。它应该接受object作为参数。我将把添加图像的细节留给你。在提供的示例中,我们从给定的目录中获取图像列表并遍历它。
使用给定图像的文件路径,我们将图像作为Stream打开。为了优化我们的相似性操作,我们在图像中找到FaceRectangle参数。由于人脸列表中每张图像应该只有一个面部,我们在Face数组中选择第一个元素,如下所示:
using (StreamfileStream = File.OpenRead(image))
{
Face[] faces = await _faceServiceClient.DetectAsync(fileStream);
FaceRectanglefaceRectangle = faces[0].FaceRectangle;
将人脸添加到人脸列表就像调用AddFaceToFaceListAsync函数一样简单。我们需要指定人脸列表 ID 和图像。图像可能来自Stream(如我们的情况)或 URL。可选地,我们可以添加用户数据和图像的人脸矩形,如下所示:
AddPersistedFaceResult addFacesResult = await _faceServiceClient.AddFaceToFaceListAsync(FaceListName.ToLower(), fileStream, null, faceRectangle);
UpdateFaceGuidsAsync();
API 调用的结果是AddPersistedFaceResult变量。它包含持久化的人脸 ID,这与DetectAsync调用中的人脸 ID 不同。添加到人脸列表中的人脸不会过期,直到被删除。
我们通过调用UpdateFaceGuidsAsync方法来完成函数。
最后,我们创建我们的FindSimilarFace函数,也接受object作为参数。为了能够搜索相似的人脸,我们需要从DetectAsync方法中获取人脸 ID(Guid变量)。这可以通过本地图像或从 URL 调用。示例代码打开文件浏览器,允许用户浏览图像。
使用人脸 ID,我们可以搜索相似的人脸,如下面的代码所示:
try {
SimilarPersistedFace[] similarFaces = await _faceServiceClient.FindSimilarAsync (findFaceGuid, FaceListName.ToLower(), 3);
我们调用FindSimilarAsync函数。第一个参数是我们指定的面部 ID。下一个参数是面部列表 ID,最后一个参数是返回的候选面部数量。默认值为 20,因此通常最好指定一个数字。
与使用人脸列表查找相似人脸相比,您可以使用Guid变量的数组。该数组应包含从DetectAsync方法检索到的人脸 ID。
在编写本文时,NuGet API 包仅支持匹配人员模式。如果您直接使用 REST API,您可以指定模式作为参数。
根据选择的模式,结果将包含相似人脸的 ID 或持久化人脸 ID。它还将包含给定人脸相似度的置信度。
要从人脸列表中删除人脸,请在 Face API 中调用以下函数:
DeleteFaceFromFaceListAsync(FACELISTID, PERSISTEDFACEID)
要删除人脸列表,请在 Face API 中调用以下函数:
DeleteFaceListAsync(FACELISTID)
要更新人脸列表,请在 Face API 中调用以下函数:
UpdateFaceListAsync(FACELISTID, FACELISTNAME, USERDATA)
对相似人脸进行分组
如果您有多个人脸图像,您可能想对人脸进行分组。通常,您将希望根据相似性对人脸进行分组,这是 Face API 提供的一项功能。
通过向 API 提供人脸 ID 列表,它将响应一个或多个组。一个组由外观相似的人脸组成。通常,这意味着这些人脸属于同一个人。找不到任何相似对应物的人脸将被放置在我们称之为MessyGroup的组中。
创建一个新的View,名为FaceGroupingView.xaml。该View应该有六个图像元素,以及对应的面部 ID 的标题和文本框。它还应该有一个用于分组命令的按钮和一个用于输出分组结果的文本框。
在相应的FaceGroupingViewModel.xaml View模型中,你应该为所有图像添加BitmapImage属性。你还应该添加面部 ID 的string属性和一个用于结果的属性。还需要一个ICommand属性。
在ViewModel的开始处,我们声明了一些private变量,如下所示:
private FaceServiceClient _faceServiceClient;
private List<string> _imageFiles = new List<string>();
private List<Guid> _faceIds = new List<Guid>();
第一个用于访问面部 API。第二个包含一个字符串列表,该列表包含我们图像的位置。最后一个列表包含检测到的面部 ID。
构造函数接受一个FaceServiceClient类型的参数。它将其分配给相应的变量并调用Initialize函数。这创建我们的ICommand对象并调用一个函数将我们的图像添加到应用程序中。
在添加图像的函数中,我们将硬编码的图像路径添加到我们的_imageFiles列表中。在这个例子中,我们添加了六个。使用for循环,我们生成每个BitmapImage属性。当我们有一个图像时,我们想要检测其中的面部:
try {
using (Stream fileStream = File.OpenRead(_imageFiles[i])) {
Face[] faces = await
_faceServiceClient.DetectAsync(fileStream);
我们不需要比生成的面部 ID 更多的数据,我们知道该 ID 在检测后存储 24 小时:
_faceIds.Add(faces[0].FaceId);
CreateImageSources(image, i, faces[0].FaceId);
}
}
假设每张图像中只有一个面部,我们将该面部 ID 添加到我们的_faceIds列表中。然后将图像、面部 ID 和循环中的当前迭代次数传递给一个新的函数CreateImageSources。这个函数包含一个基于迭代号的switch案例。根据数字,我们将图像和面部 ID 分配给相应的图像和图像 ID 属性。然后在 UI 中显示。
我们有一个按钮来分组图像。要分组图像,我们调用面部 API 的GroupAsync方法,传递一个包含面部 ID 的数组,如下所示。面部 ID 数组必须至少包含两个元素,并且不能包含超过 1,000 个元素:
private async void GroupFaces(object obj) {
try {
GroupResultfaceGroups = await _faceServiceClient.GroupAsync(_faceIds.ToArray());
响应是GroupResult类型,可能包含一个或多个组,以及混乱的组。我们检查是否有响应,然后解析它,如下所示:
if (faceGroups != null)
FaceGroupingResult = ParseGroupResult(faceGroups);
}
在查看ParseGroupResult方法之前,添加相应的catch子句并关闭GroupFaces函数。
在解析结果时,我们首先创建一个StringBuilder类来保存我们的文本。然后我们从结果中获取groups。一个组是该组中图像的面部 ID 数组。所有组都存储在一个列表中,我们将组的数量追加到StringBuilder类中,如下所示:
private string ParseGroupResult(GroupResultfaceGroups) {
StringBuilder result = new StringBuilder();
List<Guid[]>groups = faceGroups.Groups;
result.AppendFormat("There are {0} group(s)\n", groups.Count);
我们遍历组列表。在这个循环内部,我们遍历组中的每个项目。为了提高可读性,我们有一个辅助函数来从 ID 中找到图像名称。它找到我们 _faceIds 列表中的索引。然后用于图像名称,所以如果索引是 2,图像名称将是 Image 3。为了达到预期的效果,你必须按照以下逻辑顺序放置图像:
result.Append("Groups:\t");
foreach(Guid[] guid in groups)
{
foreach(Guid id in guid)
{
result.AppendFormat("{0} - ", GetImageName(id));
}
result.Append("\n");
}
GroupResult 方法也可能包含一个 MessyGroup 数组。这是一个包含该组中面部 ID 的 Guid 变量的数组。我们遍历这个数组,以与常规组相同的方式追加图像名称,如下所示代码所示:
result.Append("Messy group:\t");
Guid[] messyGroup = faceGroups.MessyGroup;
foreach(Guidguid in messyGroup)
{
result.AppendFormat("{0} - ", GetImageName(guid));
}
我们通过返回 StringBuilder 函数的文本来结束函数,这将输出到屏幕上,如下所示:
return result.ToString();
}
确保在 MainViewModel.cs 文件中创建了 ViewModel 实例。同时,确保 View 已添加到 MainView.xaml 文件中的 TabItem 属性。编译并测试应用程序。
如果你使用提供的示例图像,你可能会得到以下结果:

为我们的智能家居应用程序添加标识
作为我们智能家居应用程序的一部分,我们希望应用程序能够识别我们是谁。这样做为我们提供了从应用程序获取针对您的响应和操作的机会。
创建我们的智能家居应用程序
根据我们之前创建的 MVVM 模板创建智能家居应用程序的新项目。
在创建新项目后,添加 Microsoft.ProjectOxford.Face NuGet 包。
由于我们将在这本书中构建这个应用程序,我们将从小处开始。在 MainView.xaml 文件中,添加一个包含两个项目的 TabControl 属性。这两个项目应该是两个用户控件,一个称为 AdministrationView.xaml 文件,另一个称为 HomeView.xaml 文件。
管理控制将是我们管理应用程序不同部分的地方。主页控制将是起点和主要控制。
将相应的 ViewModel 实例添加到 Views 中。确保它们在 MainViewModel.cs 中声明和创建,正如我们在这章中看到的。在继续之前,请确保应用程序可以编译并运行。
添加要识别的人员
在我们能够继续识别一个人之前,我们需要有一些东西来识别他们。为了识别一个人,我们需要一个 PersonGroup 属性。这是一个包含几个 Persons 属性的组。
创建视图
在管理控制中,我们将执行这方面的几个操作。UI 应包含两个文本框元素、两个列表框元素和六个按钮。两个文本框元素将允许我们输入人员组名称和人员名称。一个列表框将列出我们可用的所有人员组。另一个将列出任何给定组中的所有人员。
我们为每个要执行的操作都设置了按钮,具体如下:
-
添加人员组
-
删除人员组
-
训练人员组
-
添加人员
-
删除人员
-
添加人员面部
View模型应该有两个ObservableCollection属性:一个为PersonGroup类型,另一个为Person类型。我们还应该添加三个string属性。一个将用于我们的人员组名称,另一个用于我们的人员名称。最后一个将保存一些状态文本。我们还想添加一个PersonGroup属性来选择人员组。最后,我们还想添加一个Person属性来保存所选人员。
在我们的View模型中,我们希望为FaceServiceClient方法添加一个private变量,如下面的代码所示:
private FaceServiceClient _faceServiceClient;
这应该在构造函数中分配,构造函数应接受一个FaceServiceClient类型的参数。它还应调用一个初始化函数,该函数将初始化六个ICommand属性。这些属性对应于之前创建的按钮。初始化函数应调用GetPersonGroups函数来列出所有可用的人员组,如下面的代码所示:
private async void GetPersonGroups() {
try {
PersonGroup[] personGroups = await
_faceServiceClient.ListPersonGroupsAsync();
ListPersonGroupsAsync函数不接受任何参数,如果成功执行,则返回一个PersonGroup数组,如下面的代码所示:
if(personGroups == null || personGroups.Length == 0)
{
StatusText = "No person groups found.";
return;
}
PersonGroups.Clear();
foreach (PersonGrouppersonGroup in personGroups)
{
PersonGroups.Add(personGroup);
}
}
然后,我们检查数组是否包含任何元素。如果包含,我们将清除现有的PersonGroups列表。然后我们遍历PersonGroup数组中的每个项目,并将它们添加到PersonGroups列表中。
如果没有人员组存在,我们可以通过填写一个名称来添加一个新的组。你在这里填写的名称也将用作人员组 ID。这意味着它可以包含数字和英文小写字母,"-"字符(连字符)和"_"字符(下划线)。最大长度为 64 个字符。当它被填写后,我们可以添加一个人员组。
添加人员组
首先,我们调用DoesPersonGroupExistAsync函数,指定PersonGroupName作为参数,如下面的代码所示。如果这是true,那么我们给出的名字已经存在,因此我们不允许添加它。注意我们如何在名字上调用ToLower函数。这样做是为了确保 ID 是小写的:
private async void AddPersonGroup(object obj) {
try {
if(await DoesPersonGroupExistAsync(PersonGroupName.ToLower())) {
StatusText = $"Person group {PersonGroupName} already exist";
return;
}
如果人员组不存在,我们将调用CreatePersonGroupAsync函数,如下面的代码所示。同样,我们在第一个参数中将PersonGroupName指定为小写。这代表组的 ID。第二个参数表示我们想要的名字。我们通过再次调用GetPersonGroups函数来结束函数,这样我们就可以在我们的列表中获得新添加的组:
await _faceServiceClient.CreatePersonGroupAsync (PersonGroupName.ToLower(), PersonGroupName);
StatusText = $"Person group {PersonGroupName} added";
GetPersonGroups();
}
DoesPersonGroupExistAsync函数执行一个 API 调用。它尝试调用GetPersonGroupAsync函数,将人员组 ID 指定为参数。如果结果PersonGroup列表不是null,则返回true。
要删除人员组,必须按照以下方式选择一个组:
private async void DeletePersonGroup(object obj)
{
try
{
await _faceServiceClient.DeletePersonGroupAsync (SelectedPersonGroup.PersonGroupId);
StatusText = $"Deleted person group {SelectedPersonGroup.Name}";
GetPersonGroups();
}
调用DeletePersonGroupAsync函数的 API 需要将人员组 ID 作为参数。我们从所选人员组中获取这个 ID。如果没有捕获到异常,那么调用已经成功完成,然后我们调用GetPersonGroups函数来更新我们的列表。
当从列表中选择人员组时,我们确保调用GetPersons函数。这将更新人员列表,如下所示:
private async void GetPersons()
{
if (SelectedPersonGroup == null)
return;
Persons.Clear();
try
{
Person[] persons = await _faceServiceClient.GetPersonsAsync(SelectedPersonGroup.PersonGroupId);
我们确保所选的人员组不是null。如果不是,我们清空我们的persons列表。调用GetPersonsAsync函数的 API 需要人员组 ID 作为参数。成功的调用将导致一个Person数组。
如果结果数组包含任何元素,我们将遍历它。每个Person对象都将添加到我们的persons列表中,如下面的代码所示:
if (persons == null || persons.Length == 0)
{
StatusText = $"No persons found in {SelectedPersonGroup.Name}.";
return;
}
foreach (Person person in persons)
{
Persons.Add(person);
}
}
添加新人员
如果没有人员存在,我们可以添加新的。要添加一个新的人员,必须选择一个人员组,并填写该人员的姓名。完成这些步骤后,我们可以点击添加按钮:
private async void AddPerson(object obj)
{
try
{
CreatePersonResultpersonId = await _faceServiceClient.CreatePersonAsync(SelectedPersonGroup.PersonGroupId, PersonName);
StatusText = $"Added person {PersonName} got ID: {personId.PersonId.ToString()}";
GetPersons();
}
调用CreatePersonAsync函数的 API 需要人员组 ID 作为第一个参数。下一个参数是人员的姓名。可选地,我们可以添加用户数据作为第三个参数。在这种情况下,它应该是一个字符串。当创建了一个新人员后,我们通过再次调用GetPersons函数来更新persons列表。
如果我们已选择一个人员组和一个人,那么我们将能够删除那个人,如下面的代码所示:
private async void DeletePerson(object obj)
{
try
{
await _faceServiceClient.DeletePersonAsync (SelectedPersonGroup.PersonGroupId, SelectedPerson.PersonId);
StatusText = $"Deleted {SelectedPerson.Name} from {SelectedPersonGroup.Name}";
GetPersons();
}
要删除一个人,我们调用DeletePersonAsync函数。这需要该人员所在人员组的 ID 以及我们想要删除的人员的 ID。如果没有捕获到异常,那么调用成功,然后我们调用GetPersons函数来更新我们的人员列表。
我们的管理控制界面现在看起来类似于以下截图:

将面部与人员关联
在我们能够识别一个人之前,我们需要将面部与那个人关联起来。在给定的人员组和人员被选中后,我们可以添加面部。为此,我们打开一个文件对话框。当我们有一个图像文件时,我们可以按照以下方式将该面部添加到人员中:
using (StreamimageFile = File.OpenRead(filePath))
{
AddPersistedFaceResultaddFaceResult = await _faceServiceClient.AddPersonFaceAsync(
SelectedPersonGroup.PersonGroupId,
SelectedPerson.PersonId, imageFile);
if (addFaceResult != null)
{
StatusText = $"Face added for {SelectedPerson.Name}. Remember to train the person group!";
}
}
我们将图像文件作为Stream打开。这个文件作为我们调用AddPersonFaceAsync函数的第三个参数传递。我们也可以传递一个指向图像的 URL 而不是流。
调用的第一个参数是人员所在组的人员组 ID。下一个参数是人员 ID。
可以包含的一些可选参数包括以字符串形式提供用户数据和用于图像的FaceRectangle参数。如果图像中有多于一个面部,则FaceRectangle参数是必需的。
成功的调用将导致一个AddPersistedFaceResult对象。它包含该人员的持久化面部 ID。
每个人可以与其关联最多 248 个面部。您可以添加的面部越多,您在以后收到稳定识别的可能性就越大。您添加的面部应从略微不同的角度。
训练模型
当与人员关联足够多的面部后,我们需要训练人员组。这是一个在人员或人员组发生任何更改后必须执行的任务。
当已选择一个人员组时,我们可以训练人员组,如下面的代码所示:
private async void TrainPersonGroup(object obj)
{
try
{
await _faceServiceClient.TrainPersonGroupAsync(
SelectedPersonGroup.PersonGroupId);
调用TrainPersonGroupAsync函数需要一个人员组 ID 作为参数,如下面的代码所示。它不返回任何内容,并且可能需要一段时间才能执行:
while(true)
{
TrainingStatustrainingStatus = await _faceServiceClient.GetPersonGroupTrainingStatusAsync (SelectedPersonGroup.PersonGroupId);
我们想确保训练成功完成。为此,我们在while循环中调用GetPersonGroupTrainingStatusAsync函数。此调用需要一个人员组 ID,成功的调用将返回一个TrainingStatus对象,如下面的代码所示:
if(trainingStatus.Status != Status.Running)
{
StatusText = $"Person group finished with status: {trainingStatus.Status}";
break;
}
StatusText = "Training person group...";
await Task.Delay(1000);
}
}
我们检查状态,如果它没有运行,则显示结果。如果训练仍在运行,我们等待一秒钟,然后再次运行检查。
当训练成功后,我们就可以识别人员了。
其他功能
有一些我们尚未查看的 API 调用,将在以下项目符号列表中简要提及:
-
要更新人员组,请调用以下操作;此函数不返回任何内容:
UpdatePersonGroupAsync(PERSONGROUPID, NEWNAME, USERDATA) -
要获取一个人的面部,请调用以下操作:
GetPersonFaceAsync(PERSONGROUPID, PERSONID, PERSISTEDFACEID)成功的调用返回持久化的面部 ID 和用户提供的资料。
-
要删除一个人的面部,请调用以下操作;此调用不返回任何内容:
DeletePersonFaceAsync(PERSONGROUPID, PERSONID, PERSISTEDFACeID) -
要更新一个人,请调用以下操作;此调用不返回任何内容:
UpdatePersonAsync(PERSONGROUPID, PERSONID, NEWNAME, USERDATA) -
要更新一个人的面部,请调用以下操作;此调用不返回任何内容:
UpdatePersonFaceAsync(PERSONGROUID, PERSONID, PERSISTEDFACEID, USERDATA)
识别一个人
要识别一个人,我们首先将上传一个图像。打开HomeView.xaml文件,并向 UI 添加一个ListBox元素。这将包含在识别人员时可以选择的人员组。我们需要添加一个按钮元素来查找图像、上传它并识别人员。添加一个TextBox元素以显示工作响应。为了方便起见,我们还添加了一个图像元素来显示我们正在使用的图像。
在View模型中,添加一个ObservableCollection属性,其类型为PersonGroup。我们需要为选定的PersonGroup类型添加一个属性。还要添加一个用于我们的图像的BitmapImage属性和一个用于响应的字符串属性。我们还需要一个按钮的ICommand属性。
为FaceServiceClient类型添加一个private变量,如下所示:
private FaceServiceClient _faceServiceClient;
这将在我们的构造函数中分配,该构造函数应接受一个FaceServiceClient类型的参数。从构造函数中,调用Initialize函数以初始化一切,如下面的代码所示:
private void Initialize()
{
GetPersonGroups();
UploadOwnerImageCommand = new DelegateCommand(UploadOwnerImage,CanUploadOwnerImage);
}
首先,我们调用GetPersonGroups函数来检索所有的人组。此函数调用我们之前看到的ListPersonGroupsAsync API。结果被添加到我们的PersonGroup列表的ObservableCollection参数中。
接下来,我们创建我们的ICommand对象。如果从PersonGroup列表中选择了项目,CanUploadOwnerImage函数将返回true。如果没有选择,它将返回false,我们将无法识别任何人。
在UploadOwnerImage函数中,我们首先浏览到一个图像并加载它。加载图像并获取文件路径后,我们可以开始识别图像中的人,如下面的代码所示:
using (StreamimageFile = File.OpenRead(filePath))
{
Face[] faces = await _faceServiceClient.DetectAsync(imageFile);
Guid[] faceIds = faces.Select(face =>face.FaceId).ToArray();
我们以Stream类型打开图像,如下面的代码所示。使用此方法,我们在图像中检测面部。从检测到的面部中,我们得到一个数组中的所有面部 ID:
IdentifyResult[] personsIdentified = await _faceServiceClient.IdentifyAsync (SelectedPersonGroup.PersonGroupId,
faceIds, 1);
面部 ID 数组将作为IdentifyAsync API 调用的第二个参数发送。记住,当我们检测到面部时,它将被存储 24 小时。继续使用相应的面部 ID 将确保服务知道使用哪个面部进行识别。
使用的第一个参数是我们选择的人组的 ID。调用中的最后一个参数是返回的候选人数量。由于我们不想一次识别多个人,我们指定一个。因此,我们应该确保我们上传的图像中只有一个面部。
成功的 API 调用将导致IdentifyResult参数的数组,如下面的代码所示。此数组中的每个项目都将包含候选人:
foreach(IdentifyResultpersonIdentified in personsIdentified) {
if(personIdentified.Candidates.Length == 0) {
SystemResponse = "Failed to identify you.";
break;
}
GuidpersonId = personIdentified.Candidates[0].PersonId;
我们遍历结果数组,如下面的代码所示。如果没有候选人,我们只需退出循环。然而,如果我们有候选人,我们将获取第一个候选人的PersonId参数(我们之前要求只获取一个候选人,所以这是可以的):
Person person = await faceServiceClient.GetPersonAsync(
SelectedPersonGroup.PersonGroupId, personId);
if(person != null) {
SystemResponse = $"Welcome home, {person.Name}";
break;
}
}
}
使用personId参数,我们通过 API 调用GetPersonAsync函数获取单个Person对象。如果调用成功,我们将打印一条欢迎信息给正确的人(如下面的截图所示)并退出循环:

使用人脸 API 了解你的情绪
人脸 API 允许你从面部识别情绪。
研究表明,有一些关键情绪可以归类为跨文化。这些是快乐、悲伤、惊讶、愤怒、恐惧、轻蔑、厌恶和中立。所有这些情绪都被 API 检测到,这使得您的应用程序能够通过了解用户的情绪以更个性化的方式做出响应。
我们将学习如何从图像中识别情绪,以便我们的智能家居应用程序可以了解我们的情绪。
从网络摄像头获取图像
想象一下,你的房子周围有几个摄像头。智能家居应用程序可以随时了解你的情绪。通过了解这一点,它可以利用情绪来更好地预测你的需求。
我们将向我们的应用程序添加网络摄像头功能。如果您没有网络摄像头,您可以跟随操作,但使用我们之前看到的技术加载图像。
首先,我们需要向我们的智能家居应用程序添加一个 NuGet 包。搜索OpenCvSharp3-AnyCPU并使用shimat安装包。这是一个允许处理图像的包,并且将被我们即将添加的下一个依赖项所使用。
在提供的示例代码中,有一个名为VideoFrameAnalyzer的项目。这是一个由 Microsoft 编写的项目,允许我们从网络摄像头逐帧抓取图像。使用它,我们能够在我们的应用程序中分析情感。我们将执行的使用案例如下:

在我们的HomeView.xaml文件中,添加两个新按钮。一个用于启动网络摄像头,另一个用于停止它。
在相应的View模型中,为每个按钮添加两个ICommand属性。还要添加以下private成员:
private FrameGrabber<CameraResult> _frameGrabber;
private static readonly ImageEncodingParam[] s_jpegParams = {
new ImageEncodingParam(ImwriteFlags.JpegQuality, 60)
};
第一个是一个FrameGrabber对象,它来自VideoFrameAnalyzer项目。static成员是一个图像参数数组,用于获取网络摄像头图像时使用。此外,我们还需要添加一个CameraResult类,它应该位于ViewModel文件中。
我们将EmotionScores初始化为null,如下所示代码所示。这样做是为了确保新的情感分数总是从最新的分析结果中分配:
internal class CameraResult {
public EmotionScores EmotionScores { get; set; } = null;
}
在构造函数中添加_frameGrabber成员的初始化,并在Initialization函数中添加以下内容:
_frameGrabber.NewFrameProvided += OnNewFrameProvided;
每次从摄像头提供新的帧时,都会引发一个事件。
当我们收到新的帧时,我们希望从它创建一个BitmapImage以在 UI 中显示。为此,我们需要从当前调度器调用操作,因为事件是从后台线程触发的,如下所示代码:
private void OnNewFrameProvided(object sender, FrameGrabber<CameraResult>.NewFrameEventArgs e) {
Application.Current.Dispatcher.Invoke(() => {
BitmapSource bitmapSource = e.Frame.Image.ToBitmapSource();
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
MemoryStream memoryStream = new MemoryStream();
BitmapImage image = new BitmapImage();
我们获取Frame的BitmapSource并创建一些所需的变量。
使用我们创建的encoder,我们添加bitmapSource并将其保存到memoryStream中,如下所示:
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
encoder.Save(memoryStream);
然后,将此memoryStream分配给我们在以下代码中创建的BitmapImage。这反过来又分配给ImageSource,它将在 UI 中显示帧:
memoryStream.Position = 0;
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = memoryStream;
image.EndInit();
memoryStream.Close();
ImageSource = image;
由于此事件将被触发很多次,我们将在 UI 中获得一个流畅的流,并且它看起来就像是一个直接的视频流。
在我们的Initialization函数中,我们还需要为按钮创建我们的ICommand,如下所示:
StopCameraCommand = new DelegateCommand(StopCamera);
StartCameraCommand = new DelegateCommand(StartCamera, CanStartCamera);
要能够启动摄像头,我们需要选择一个人员组,并且至少有一个摄像头可用:
private bool CanStartCamera(object obj) {
return _frameGrabber.GetNumCameras() > 0 && SelectedPersonGroup != null;
}
要启动摄像头,我们需要指定要使用哪个摄像头以及我们希望多久触发一次分析,如下所示代码:
private async void StartCamera(object obj) {
_frameGrabber.TriggerAnalysisOnInterval(TimeSpan.FromSeconds(5));
await _frameGrabber.StartProcessingCameraAsync();
}
如果在StartProcessingCameraAsync中没有指定摄像头,则默认选择第一个可用的摄像头。
我们将很快回到这个过程的分析部分。
要停止摄像头,我们运行以下命令:
private async void StopCamera(object obj) {
await _frameGrabber.StopProcessingAsync();
}
让智能屋了解你的情绪
我们现在有了网络摄像头提供的视频可供我们使用。
在FrameGrabber类中,有一个Func,它将被用于分析函数。我们需要创建一个函数,该函数将被传递到这个函数中,以使情感能够被识别。
创建一个新的函数,EmotionAnalysisAsync,它接受一个VideoFrame作为参数。返回类型应该是Task<CameraResult>,并且该函数应该被标记为async。
我们作为参数获得的frame用于创建一个包含当前帧的MemoryStream。这将是以 JPG 文件格式。我们将在这张图片中找到一个脸,我们想要确保我们指定我们想要使用以下代码来指定情感属性:
private async Task<CameraResult> EmotionAnalysisAsync (VideoFrame frame) {
MemoryStream jpg = frame.Image.ToMemoryStream(".jpg", s_jpegParams);
try {
Face[] face = await _faceServiceClient.DetectAsync(jpg, true, false, new List<FaceAttributeType>
{ FaceAttributeType.Emotion });
EmotionScores emotions = face.First()?.FaceAttributes?.Emotion;
成功调用将返回一个包含所有情感分数的对象,如下面的代码所示。这些分数就是我们想要返回的内容:
return new CameraResult {
EmotionScores = emotions
};
捕获可能抛出的任何异常,当它们发生时返回null。
我们需要将Initialize函数分配给Func。我们还需要在每次有新的结果时添加一个事件处理程序。
当获得新的结果时,我们抓取接收到的EmotionScore,如下面的代码所示。如果它是null或不包含任何元素,那么我们不想做任何其他事情:
_frameGrabber.NewResultAvailable += OnResultAvailable;
_frameGrabber.AnalysisFunction = EmotionAnalysisAsync;
private void OnResultAvailable(object sender, FrameGrabber<CameraResult>.NewResultEventArgs e)
{
var analysisResult = e.Analysis.EmotionScores;
if (analysisResult == null)
return;
在以下代码中,我们解析AnalyseEmotions中的情感分数,我们稍后会看到:
string emotion = AnalyseEmotions(analysisResult);
Application.Current.Dispatcher.Invoke(() => {
SystemResponse = $"You seem to be {emotion} today.";
});
}
使用AnalyseEmotions的结果,我们向结果打印一个字符串来指示当前情绪。这需要从当前的分发器中调用,因为事件是在另一个线程中触发的。
为了以可读的格式获取当前情绪,我们在AnalyseEmotions中解析情感分数如下:
private string AnalyseEmotions(Scores analysisResult) {
string emotion = string.Empty;
var sortedEmotions = analysisResult.ToRankedList();
string currentEmotion = sortedEmotions.First().Key;
使用我们得到的Scores,我们调用一个ToRankedList函数。这将返回一个包含每个情感及其对应置信度的KeyValuePair列表。第一个将是可能性最高的,第二个将是第二可能性最高的,依此类推。我们只关心可能性最高的那个,所以我们选择它。
在选择了最高的情感分数后,我们使用一个switch语句来找到正确的情感。这将返回并打印到结果中,如下所示:
switch(currentEmotion)
{
case "Anger":
emotion = "angry";
break;
case "Contempt":
emotion = "contempt";
break;
case "Disgust":
emotion = "disgusted";
break;
case "Fear":
emotion = "scared";
break;
case "Happiness":
emotion = "happy";
break;
case "Neutral":
default:
emotion = "neutral";
break;
case "Sadness":
emotion = "sad";
break;
case "Suprise":
emotion = "suprised";
break;
}
return emotion;
}
最后一个拼图是要确保分析在指定的时间间隔内执行。在StartCamera函数中,在调用StartProcessingCamera之前添加以下行:
_frameGrabber.TriggerAnalysisOnInterval(TimeSpan.FromSeconds(5));
这将触发每五秒调用一次情感分析。
当我脸上带着微笑时,应用程序现在知道我是快乐的,并且可以相应地提供进一步的交互。如果我们编译并运行示例,我们应该得到如下截图所示的结果:

当我的情绪变为中性时,应用程序也会检测到这一点:

自动调节用户内容
使用内容审核 API,我们可以对用户生成的内容进行监控。该 API 旨在协助标记和评估及过滤冒犯性和不受欢迎的内容。
内容审核 API 的类型
我们将在本节中快速介绍审核 API 的关键特性。
注意
所有 API 的文档参考可以在docs.microsoft.com/nb-no/azure/cognitive-services/content-moderator/api-reference找到。
图像审核
图像审核 API 允许您对成人和不适当的内容进行审核。它还可以提取图像中的文本内容并检测图像中的面部。
当使用 API 评估不适当的内容时,API 将接受一个图像作为输入。根据图像,它将返回一个布尔值,指示图像是否适当。它还将包含一个介于 0 和 1 之间的对应置信度分数。布尔值是基于一组默认阈值设置的。
如果图像包含任何文本,API 将使用 OCR 提取文本。然后,它将寻找与文本审核相同的成人或低俗内容,我们将在稍后讨论。
一些基于内容的应用程序可能不想显示任何可识别个人信息,在这种情况下,检测图像中的面部可能很明智。基于面部检测评估中检索到的信息,您可以确保用户内容中不包含任何人的图像。
文本审核
使用文本审核 API,您可以对自定义和共享的文本列表进行筛选。它能够检测文本中的可识别个人信息和粗俗语言。在这种情况下,可识别个人信息是指电子邮件地址、电话号码和邮寄地址等信息的存在。
当您提交要审核的文本时,如果未声明,API 可以检测使用的语言。筛选文本将自动纠正任何拼写错误(以捕捉故意拼错的单词)。结果将包含文本中亵渎和可识别个人信息的位置,以及原始文本、自动更正后的文本和语言。使用这些结果,您可以适当地审核内容。
审核工具
有三种方式可以通过内容审核员来审核内容:
-
人工审核: 使用团队和社区手动审核所有内容
-
自动审核: 利用机器学习和人工智能进行大规模审核,无需人工交互
-
混合审核: 前两种方法的结合,其中人们通常偶尔进行审查
常用的场景是最后一个。这是使用机器学习来自动化审查过程,并且团队可以审查。微软创建了一个审查工具来简化此过程。这允许您在网页浏览器中查看所有待审查的项目,同时在使用您应用程序中的 API。我们将在下一节中探讨这个工具。
使用
l,前往contentmoderator.cognitive.microsoft.com/。从这里,您可以使用您的 Microsoft 账户登录。在首次登录时,您需要通过添加您的姓名到账户进行注册。然后,您将创建一个审查团队,如下面的截图所示:

您可以通过选择区域并输入团队名称来完成此操作。您可以可选地输入其他应成为团队一部分的人的电子邮件地址。点击创建团队。
进入后,您将看到以下仪表板:

您将看到需要审查的图像和文本内容的总数。您还将看到已完成和待审查的审查总数。仪表板还会列出已完成审查的用户以及用于内容的任何标签。
通过在菜单中选择尝试选项,您可以选择上传图像或文本以在线执行审查。通过上传图像或在文本框中输入示例文本来完成此操作。完成后,您可以选择审查选项,您将看到以下屏幕:

如果给定内容是成人内容或种族主义内容,您可以分别点击a或r按钮。对于文本,任何亵渎性语言都会显示出来。一旦完成标记审查,点击下一步。这将通过审查给定内容的过程。
其他工具
除了 API 和审查工具之外,还有两个其他工具您可以使用,如下所示:
-
列表管理 API:使用自定义的图像和文本列表来审查您不希望重复扫描的已识别内容
-
工作流 API:使用此 API,您可以定义条件逻辑和操作来指定您特定内容使用的策略
要使用任何这些 API 或使用审查 API,您可以对特定的 REST API 进行调用。为此,您需要使用 API 密钥和基本 URL。这些设置可以在审查工具网站的设置 | 凭据下找到,如下面的截图所示:

构建您自己的图像分类器
自定义视觉服务允许您构建自己的图像分类器。可能存在需要特殊图像来使用图像 API 的情况。这些情况可能来自工厂,您需要识别的设备不太可用。您可以从构建原型开始,使用尽可能少的
构建分类器
要构建一个分类器,你需要创建一个新的项目。这样做将允许你指定图像将属于哪个类别。你还将选择分类类型和项目类型。
继续前进,你需要上传图像。这可以通过网页或通过 REST API 完成。所有图像都必须标记,以便分类器以后能够识别相似图像。
一旦所有图像(至少 50 张)上传完毕,你必须训练你的模型。一旦训练完成,你将看到每个标签的精确百分比。这是对模型准确度的测量。
改进模型
在网站上,你可以测试你的模型。这样做将允许你上传图像,这些图像将被模型分类。如果模型表现不佳,你可以改进模型。
改进模型涉及上传更多图像。以下是一些改进模型的通用指南:
-
有足够的图像
-
确保标签之间的平衡良好(以便每个标签都有相同数量的图像)
-
使用多样化的图像进行训练
-
使用用于预测的图像
-
检查预测结果
使用训练好的模型
一旦你对模型满意,你可以用它来进行预测。模型可以通过以下两种方式之一使用:
-
使用 REST API
-
导出为模型文件
第一种选择涉及上传一个图像。调用为你模型生成的端点,以及图像数据,将导致一个预测。结果将包含按概率排序的预测标签。
第二种选择允许你在离线状态下运行预测。这意味着你可以利用不同的框架,如 TensorFlow、CoreML 和 ONNX,在不同的平台上。如何使用这些框架与模型结合使用超出了本书的范围。使用离线模型的缺点是,与在线版本相比,准确度可能略有下降。
摘要
在本章中,我们深入研究了视觉 API 的一部分。你首先学习了如何获取图像的良好描述。接下来,你学习了如何在图像中识别名人和文本,以及如何生成缩略图。随后,我们转向了面部 API,在那里我们获得了更多关于检测到的面部信息。我们了解到如何验证两个面部是否相同。之后,你学习了如何找到相似的面部并将相似的面部分组。然后我们在智能屋应用中添加了识别功能,使其能够知道我们是谁。我们还添加了识别面部情绪的能力。我们快速浏览了内容审核器,看看你如何为用户生成的内容添加自动审核。最后,我们简要介绍了自定义视觉服务,以及如何使用它来生成特定的预测模型。
下一章将继续介绍最终视觉 API。我们将关注视频,了解视频索引器 API 能提供什么。
第三章:分析视频
在上一章中,我们探讨了用于处理图像的不同 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。
第七章. 为企业构建推荐系统
“通过利用 Azure 机器学习和推荐 API,我们为零售商推出了一种新的个性化商业体验,这有助于在任何渠道上增加购物者的转化率和参与度。”
– Frank Kouretas,Orckestra 的首席产品官
在上一章中,我们介绍了剩余的语言 API。在本章中,我们将探讨推荐解决方案模板。这是一个包含运行推荐解决方案所需资源的 Microsoft Azure 模板。这是一个非常适合电子商务应用的解决方案,您可以根据不同的标准推荐不同的商品。在在线商店中,如果按照规则集进行推荐,这个过程可能会非常耗时。推荐解决方案允许我们利用机器学习的力量获得良好的推荐,从而可能增加销售额。
本章将涵盖以下主题:
-
部署推荐解决方案模板
-
训练推荐模型
-
消费推荐
提供个性化推荐
如果您运营一个电子商务网站,一个对客户来说很棒的功能就是推荐。使用推荐解决方案,您可以轻松地添加这个功能。利用 Microsoft Azure 机器学习,API 可以被训练以识别应该被推荐的商品。
推荐有两种常见的场景,如下所示:
-
商品到商品推荐(I2I):I2I 是某些商品经常在查看其他商品之后的场景。通常,这将以“访问此商品的访客也访问了此其他商品”的形式出现。
-
客户到商品推荐(U2I):U2I 是利用客户的先前行为来推荐商品的场景。例如,如果您销售电影,那么您可以根据客户的先前电影选择推荐其他电影。
使用推荐解决方案的一般步骤如下:
-
在 Azure 中部署模板
-
导入目录数据(您的电子商务网站中的商品)
-
导入使用数据
-
训练推荐模型
-
消费推荐
如果您还没有这样做,您应该在 portal.azure.com 注册一个 API 密钥。
在 Azure 中部署推荐解决方案模板
要部署推荐解决方案,您必须有一个有效的 Microsoft Azure 订阅。
转到 github.com/Microsoft/Product-Recommendations/tree/master/deploy 开始部署。点击如图所示的“部署到 Azure”:

这将带您进入 Microsoft Azure 的以下页面:

输入所需信息,接受条款和条件,然后点击 购买。这将开始部署推荐解决方案所需资源的流程。
几分钟后,部署完成。现在您可以上传数据以训练模型。
导入目录数据
部署解决方案后,我们可以添加目录数据。通常,您会在这里添加数据库中的项目。项目需要作为文件上传。文件需要是 CSV 格式。
以下表格描述了您目录中每个项目所需的数据:
| 名称 | 描述 |
|---|---|
| 项目 ID | 给定项目的唯一标识符 |
| 项目名称 | 项目的名称 |
| 项目类别 | 项目的类别,例如硬件、软件、书籍类型等 |
此外,还有一些可选的数据字段。这些字段在以下表格中进行了描述:
| 名称 | 描述 |
|---|---|
| 描述 | 项目的描述 |
| 功能列表 | 一个以逗号分隔的功能列表,可以增强推荐 |
包含所有数据的文件可能包含如下所示的项目:
C9F00168, Kiruna Flip Cover, Accessories, Description of item, compatibility = lumia, hardware type = mobile
通常,添加功能会更好,因为这可以提高推荐的质量。如果不存在任何功能,那么使用率较低的新项目不太可能被推荐。
功能应该是分类的。这意味着一个功能可以是一个价格范围。单独的价格不会作为一个好的功能。
您可以为每个项目添加多达 20 个功能。当上传包含项目功能的目录时,您需要执行排名构建。这将为每个功能进行排名,通常排名较高的功能更适合使用。
本章的代码示例包含一个示例目录。我们将使用此目录作为以下示例。或者,您可以从 Microsoft 下载一些数据,aka.ms/RecoSampleData。我们想使用 MsStoreData.zip 中的数据。
下载完文件后,我们可以将目录上传到我们的存储中。这可以通过前往您新创建的存储帐户并为目录创建一个新的 blob 容器来完成,如下面的截图所示:

点击 上传,浏览到您下载的样本文件,并选择 catalog.csv 文件。这将上传目录。
备注
注意,目录文件不是必需的,但建议您上传它,以便将其提供给模型。
目录中的项目最大数量为 100,000。任何给定的目录文件都不能超过 200 MB。如果您的文件更大,并且您还有更多项目,您可以上传多个文件。
导入使用数据
下一个步骤是我们需要上传使用数据。这是一个描述您过去客户所有交易的文件。文件包含行,其中每行是一个包含数据的逗号分隔行,表示交易。
必需的数据字段如下:
| 名称 | 描述 |
|---|---|
| 用户 ID | 每个客户的唯一标识符 |
| 项目 ID | 与目录相关联的项目唯一标识符 |
| 时间 | 交易时间 |
此外,还可以有一个名为Event的字段。这描述了交易类型。此字段的允许值是Click、RecommendationClick、AddShopCart、RemoveShopCart和Purchase。
根据目录中的先前示例,用法文件中的一行可能如下所示:
00030000D16C4237, C9F00168, 2015/08/04 T 11:02:37, Purchase
用法文件的最大文件大小为 200 MB。
推荐质量依赖于使用数据的数量。通常,你应该为每个项目注册大约 20 笔交易。这意味着如果你在目录中有 100 个项目,你应该在用法文件中争取达到 2,000 笔交易。
注意,当前 API 接受的交易最大数量为 500 万。如果新增交易超过此最大值,最旧的数据将被删除。
再次,你可以在aka.ms/RecoSampleData找到示例用法文件。创建另一个名为usage的 blob 容器,然后点击上传。上传样本文件夹中的所有用法文件。
训练模型
在目录和用法数据就绪后,是时候训练一个模型了。
开始训练
要开始训练过程,我们需要向新创建的应用服务上的端点发起 API 调用。这可以使用工具,如 Postman,或通过您自己的应用程序完成。我们将使用 Postman 来完成本书的目的。
备注
下载 Postman,请访问www.getpostman.com/。
可以通过向以下 URL 发送 POST 请求来开始训练过程:
https://<service_name>.azurewebsites.net/api/models
请求必须包含一个头信息,x-api-key,包含你的 API 密钥。它还必须包含另一个头信息,Content-Type,应设置为application/json。
此外,请求必须包含包含以下内容的正文:
| 属性 | 必需 | 描述 |
|---|---|---|
description |
否 | 文本描述。 |
blobContainerName |
是 | 存储目录和用法数据的 blob 容器的名称。 |
usageRelativePath |
是 | 要用于训练的虚拟目录(包含用法文件)或特定用法文件的相对路径。 |
catalogFileRelativePath |
否 | 目录文件的相对路径。 |
evaluationUsageRelativePath |
否 | 要用于评估的虚拟目录(包含用法文件)或特定用法文件的相对路径。 |
supportThreshold |
否 | 模型保守程度,以建模要考虑的项目共现次数衡量。 |
cooccurrenceUnit |
否 | 指示在计数共现之前如何分组使用事件。 |
similarityFunction |
否 | 定义要使用的相似度函数。可以是 Jaccard、Cooccurrence 或 Lift。 |
enableColdItemPlacement |
否 | 这将是 true 或 false。指示是否应通过特征相似性推送冷项目。 |
enableColdToColdRecommendations |
否 | 这将是 true 或 false。指示是否应计算冷项目对之间的相似性。 |
enableUserAffinity |
否 | 这将是 true 或 false。定义是否应将事件类型和时间作为结果的输入。 |
enableBackfilling |
否 | 这将是 true 或 false。如果没有足够的相关项返回,这将使用热门项进行回填。 |
allowSeedItemsInRecommendations |
否 | 这将是 true 或 false。确定输入项是否可以作为结果返回。 |
decayPeriodInDays |
否 | 事件发生的天数衰减期。事件发生的时间越长,该事件的重要性就越低。 |
enableUserToItemRecommendations |
否 | 这将是 true 或 false。如果为 true,则在请求个性化推荐时将考虑用户 ID。 |
一个成功的调用可能会产生以下结果:|
|
返回的 id 字段可用于检查训练状态。|
验证训练完成情况|
使用前一个请求返回的 ID,我们现在可以运行一个 GET 请求到以下端点:|
https://<service_name>.azurewebsites.net/api/models/<model_id>
此请求需要一个包含您的 API 密钥的标题,x-api-key。一个成功的请求可能会给出以下响应:|
|
GET 请求的响应|
如您所见,有一个 modelStatus 字段。一旦这是 Completed,模型就训练完成并准备好使用。您还将看到其他详细信息,例如训练时长等统计数据。|
小贴士|
如果您希望使用用户界面进行模型训练,您可以访问 https://<your_service>.azurewebsites.net/ui。|
消费推荐|
要使用我们刚刚创建的推荐模型,我们将创建一个新的示例应用程序。使用我们之前创建的 MVVM 模板创建此应用程序。|
在撰写本文时,没有为推荐 API 提供客户端包。这意味着我们需要依赖网络请求,正如我们在第六章中看到的,理解文本。为了加快开发时间,请从第六章的示例代码中复制 WebRequest.cs 文件。将此文件粘贴到 Model 文件夹中,并确保更新命名空间。|
注意|
记得添加对 System.Web 和 System.Runtime.Serialization 的引用。|
由于不需要太多的 UI,我们将所有内容添加到 MainView.xaml 文件中。我们需要两个 ComboBox 元素。这些将列出我们的推荐模型和目录项。我们还需要一个 Button 元素来获取推荐和一个 TextBox 元素来显示结果推荐。
相应的 ViewModel,MainViewModel.cs,将需要属性来对应 UI 元素。添加一个 ObservableCollection 的 RecommendationModel 类型来保存我们的模型。我们稍后会查看这个类型。我们需要一个 RecommendationModel 类型的属性来保存选中的模型。添加一个 ObservableCollection 属性的 Product 类型,以及相应的 Product 属性用于可用和选中的属性。我们还需要一个 string 类型的属性用于结果和一个 ICommand 类型的属性用于我们的按钮。
添加一个 private 类型的 WebRequest 成员,以便我们可以调用 API。
在 Model 文件夹中添加一个名为 Product 的新文件。为了使用目录中的项目,我们将加载目录文件到应用程序中,为每个项目创建一个 Product。确保 Product 看起来如下:
public class Product {
public string Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public Product(string id, string name, string category) {
Id = id;
Name = name;
Category = category;
}
}
我们需要一个项目的 Id,以及 Name 和 Category。
构造函数应创建一个 WebRequest 对象,如下面的代码所示:
public MainViewModel()
{
_webRequest = new WebRequest ("https://<YOUR_WEB_SERVICE>.azurewebsites.net/api/models/", "API_KEY_HERE");
RecommendCommand = new DelegateCommand(RecommendBook, CanRecommendBook);
Initialize();
}
当我们创建 WebRequest 对象时,我们指定推荐端点和我们的 API 密钥。RecommendCommand 语句是 ICommand 对象,作为一个 DelegateCommand。我们需要指定要执行的操作以及允许执行命令的条件。如果我们已选中推荐模型和产品,我们应该可以执行该命令。
Initialize 语句将确保我们获取我们的推荐模型和产品,如下面的代码所示:
private async void Initialize() {
await GetModels();
GetProducts();
}
GetModels 方法将调用 API,如下面的代码所示:
private async Task GetModels()
{
List<RecommandationModel> models = await _webRequest.GetModels(HttpMethod.Get);
此调用是一个 GET 请求,因此在 GetModels 中指定这一点。成功的调用应导致一个 JSON 响应,然后我们将其反序列化为 RecommendationModel 对象。这是一个数据合约,因此在一个名为 Contracts 的文件夹中添加一个名为 Models.cs 的文件。
成功的结果将给出以下输出:
[
{
"id": "string",
"description": "string",
"creationTime": "string",
"modelStatus": "string"
}
{...}
{...}
]
我们有一个 models 数组。这个数组中的每个项目都有一个 id、name、description、createdDateTime、activeBuildId 和 catalogDisplayName。确保 RecommendationModels 类包含这些数据。
如果调用成功,我们将模型添加到可用模型的 ObservableCollection 中,如下面的代码所示:
foreach (RecommandationModel model in models) {
AvailableModels.Add(model);
}
SelectedModel = AvailableModels.FirstOrDefault();
}
当所有项目都添加后,我们将 SelectedModel 设置为第一个可用选项。
要添加目录中的项目,我们需要从目录文件中读取。在书中提供的示例代码中,此文件被添加到项目中并复制到输出目录。GetProducts 方法将如下所示:
private void GetProducts() {
try {
var reader = new StreamReader (File.OpenRead("catalog.csv"));
while(!reader.EndOfStream) {
string line = reader.ReadLine();
var productInfo = line.Split(',');
AvailableProducts.Add(new Product(productInfo[0], productInfo[1], productInfo[2]));
}
SelectedProduct = AvailableProducts.FirstOrDefault();
}
catch(Exception ex) {
Debug.WriteLine(ex.Message);
}
}
这是一个基本的文件操作,从目录中读取每一行。对于每个项目,我们获取所需的信息,为每个项目创建一个Product。然后将其添加到ObservableCollection属性的AvailableProducts中,SelectedProduct是第一个可用的。
现在我们有了我们的推荐模型和我们的产品,我们可以执行推荐请求,如下面的代码所示:
private async void RecommendProduct(object obj)
{
List<RecommendedItem> recommendations = await _webRequest.RecommendItem(HttpMethod.Get, $"{SelectedModel.id}/recommend?item={SelectedProduct.Id}");
获取推荐信息的调用是一个GET请求。这需要我们添加itemIds。
itemIds参数必须是所选产品的 ID。
我们在_webRequest对象上调用RecommendItem方法。这是一个GET请求,我们需要在查询字符串中指定SelectedModel的 ID。我们还需要在查询字符串中添加一些内容,以便到达正确的端点。成功的响应将产生 JSON 输出,如下所示:
[
{
"recommendedItemId": "string",
"score": "float"
},
{...}
{...}
]
结果由一个对象数组组成。每个项目都将有一个recommendedItemId和score。分数表示客户想要给定项目的可能性。
此结果应反序列化为RecommandedItem类型的数据契约列表,因此请确保您在Contracts文件夹中添加此内容。
当我们成功调用后,我们希望在 UI 中显示如下:
if(recommendations.Count == 0) {
Recommendations = "No recommendations found";
return;
}
StringBuilder sb = new StringBuilder();
sb.Append("Recommended items:\n\n");
首先,我们检查是否有任何推荐。如果没有,我们不会继续。如果有项目,我们创建一个StringBuilder来格式化我们的输出,如下所示:
foreach(RecommendedItem recommendedItem in recommendations) {
sb.AppendFormat("Score: {0}n", recommendedItem.score);
sb.AppendFormat("Item ID: {0}\n", item.id);
sb.Append("n");
}
Recommendations = sb.ToString();
}
我们遍历所有的recommendedItems。我们输出score和id。这将打印在 UI 上。
成功的测试运行可能给出以下结果:

有几个特殊情况需要注意:
-
如果项目列表中只有一个不存在于目录中的项目,则返回空结果
-
如果项目列表中包含一些不在目录中的项目,则从查询中删除这些项目
-
如果项目列表中只包含冷门商品(与它们没有关联使用数据的商品),则返回最受欢迎的推荐
-
如果项目列表中包含一些冷门商品,则将返回其他商品的推荐
基于先前活动推荐项目
要根据用户活动进行推荐,我们需要一个用户列表。由于这样的列表仅为了示例而创建可能会过于繁琐,我们将只查看制作此类推荐所需的步骤和参数。
此用途的端点略有不同,因为它是一个另一个GET调用。在代码中,它将如下所示:
$"{SelectedModel.id}/recommend/user?{queryString.ToString()}"
查询字符串中的参数如下:
| 参数 | 描述 |
|---|---|
userId (required) |
给定用户的唯一标识符。 |
numberOfResults (required) |
返回的推荐数量。 |
itemsIds (optional) |
选定项目(的)列表或单个 ID。 |
includeMetadata (optional) |
如果为 true,则将包括项目的元数据。 |
buildId (optional) |
一个标识我们想要使用的构建的数字。如果没有指定,则使用活动构建。 |
成功的调用将产生与其他推荐模型相同的 JSON 输出。推荐的项目当然将基于用户的过去活动。
注意
注意,为了能够使用此功能,在创建模型构建时必须将 U2I 设置为true。
摘要
在本章中,我们深入探讨了推荐 API。我们学习了如何使用现有的目录和用法数据设置推荐模型。使用这些模型,我们学习了如何在简单的示例应用程序中利用它们。
在下一章中,我们将从知识 API 开始。我们将学习如何构建自然语言查询并评估查询表达式。此外,我们还将学习如何为自然语言查询添加自动补全功能。
第八章. 以自然方式查询结构化数据
在上一章中,我们学习了如何利用当前上下文来扩展我们对某个主题的知识。在本章中,我们将继续讨论关于知识 API 的内容。更具体地说,我们将学习如何探索学术论文和期刊之间的关系。我们将看到如何解释自然语言查询,并检索查询表达式。使用这些表达式,我们将学习如何找到学术实体。然后,我们将更多地关注如何在个人设置中建立此类服务。在本章结束时,我们将探讨 QnA Maker,看看如何从现有内容中创建 FAQ 服务。
本章将涵盖以下主题:
-
使用项目学术知识解释自然语言用户查询
-
使用自动完成功能协助用户进行查询
-
使用自动完成查询检索学术实体
-
从查询中计算学术实体的分布
-
使用自己的模式托管项目知识探索服务
-
使用 QnA Maker 从现有内容创建 FAQ 服务
使用学术 API 获取学术内容
Microsoft Academic Graph (MAG) 是一个用于网络规模、异构实体图的数据库。实体模型了学术活动,并包含诸如研究领域、作者、机构等信息。
MAG 中的数据是从 Bing 网络索引中索引的。由于这些数据持续被索引,因此数据始终是最新的。
通过使用项目学术知识 API,我们可以访问这个知识库。此 API 允许我们结合搜索建议、研究论文图搜索结果和直方图分布。该 API 支持知识驱动和交互式对话。
当用户搜索研究论文时,API 可以提供查询补全。它可能会根据输入建议查询。使用完整的查询,我们可以评估查询表达式。这将从知识库中检索一组匹配的论文实体。
设置示例项目
为了测试项目学术知识,我们首先需要创建一个新的示例项目。我们将从第一章中创建的 MVVM 模板开始,即使用 Microsoft 认知服务入门。
项目学术知识没有提供任何客户端包。这意味着我们需要自己调用 API。从智能家居应用程序的Model文件夹中复制WebRequest.cs文件,并将其粘贴到新创建的项目中的Model文件夹中。确保您已正确修改命名空间。
为了能够编译此内容,我们需要添加对System.Web和System.Runtime.Serializable的引用。我们还将处理 JSON,因此请通过 NuGet 包管理器添加Newtonsoft.Json包。
由于这将是本示例项目中唯一测试的 API,我们可以在 MainView.xaml 文件中添加 UI 元素。现在打开此文件。
我们的 View 应该有一个 TextBox 元素用于我们的输入查询。它应该有一个 ComboBox 元素来列出建议的查询表达式。我们需要三个 Button 元素,一个用于 Interpret,一个用于 Evaluate,一个用于 Histogram,这些都是我们将要执行的功能。最后但同样重要的是,我们需要一个 TextBox 元素来显示我们的结果。
在 MainViewModel.cs 文件中,我们需要添加相应的属性。添加三个 string 类型的属性,一个用于输入查询,一个用于结果,一个用于选定的查询表达式。添加一个 ObservableCollection 属性,类型为 string,用于我们的可用查询表达式。我们还需要三个 ICommand 属性,分别对应我们的按钮。
为我们的 WebRequest 对象添加一个私有成员。使构造函数看起来如下:
public MainViewModel()
{
_webRequest = new WebRequest("https://api.labs.cognitive.microsoft.com/academic/v1.0/",
"API_KEY_HERE");
InterpretCommand = new DelegateCommand(Interpret, CanInterpret);
EvaluateCommand = new DelegateCommand(Evaluate, CanExecuteCommands);
CalculateHistogramCommand = new DelegateCommand (CalculateHistogram,
CanExecuteCommands);
}
注意
如果你还没有这样做,请在 labs.cognitive.microsoft.com/en-us/project-academic-knowledge 上注册 API 密钥并点击 Subscribe 按钮。
当我们在查询文本框中输入任何文本时,CanInterpret 参数应返回 true。如果已选择查询表达式,则 CanExecuteCommands 参数应返回 true。我们将在接下来的章节中介绍 Interpret、Evaluate 和 CalculateHistogram 参数。
在继续之前,请确保应用程序可以编译并运行。
解释自然语言查询
API 用来评估查询的查询表达式不是自然语言格式。为了确保用户可以以自然的方式提出查询,我们需要解释他们的输入。
当调用 API 的 Interpret 功能时,它接受一个查询字符串。这将返回并格式化以反映用户意图,使用学术语法。此外,此功能可以在用户编写时调用,以提供交互式体验。
请求是一个 GET 请求,如下面的代码所示:
private async void Interpret(object obj)
{
var queryString = HttpUtility.ParseQueryString(string.Empty);
queryString["query"] = InputQuery;
queryString["complete"] = "1";
//queryString["count"] = "10";
//queryString["offset"] = "0";
//queryString["timeout"] = "1000";
//queryString["model"] = "latest";
我们通过创建一个 queryString 变量来开始调用。我们可以输入的参数如下表所示:
| 参数 | 描述 |
|---|---|
query (必需) |
用户的查询。 |
complete (可选) |
如果设置为 1,则服务将使用查询作为前缀返回建议。值为 0 表示将没有自动完成。 |
count (可选) |
返回的最大解释数量。 |
offset (可选) |
首个解释的索引。如果预期有很多结果并且需要添加分页,这很有用。 |
timeout (可选) |
指定的超时时间(毫秒)。只有在此限制之前找到的结果将被返回。 |
model (可选) |
你想要查询的模型名称。默认为最新模型。 |
我们调用 API 获取解释,如下面的代码所示:
InterpretResponse response = await _webRequest.MakeRequest<object,
InterpretResponse>(HttpMethod.Get, $"interpret?{queryString.ToString()}");
if (response == null || response.interpretations.Length == 0)
return;
由于这是一个 GET 请求,我们不需要指定任何请求体。然而,我们期望结果被序列化为一个 InterpretResponse 对象。这是一个数据合约,包含来自结果属性。
成功调用 API 将导致 JSON 响应,如下所示:
{
"query": "papers by jaime", "interpretations": [
{
"prob": 2.429e-006,
"parse": "<rule id="#GetPapers"> papers by <attr name="academic#AA.AuN">
jaime teevan </attr></rule>",
"rules": [
{
"name": "#GetPapers",
"output": {
"type": "query",
"value": "Composite(AA.AuN=='jaime teevan')"
}
}]
}]
}
结果包含原始 query。它还包含一个包含 interpretations 的数组。该数组中的每个项目都由以下表格中的数据组成:
| 数据字段 | 描述 |
|---|---|
prob |
这是当前解释正确的概率。范围从 0 到 1,其中 1 是最高的。 |
parse |
这是一个 XML 字符串,显示了字符串每个部分的解释。 |
rules |
这是一个包含一个或多个定义的规则的数组。对于学术 API,始终有一个规则。 |
rules[x].name |
这是当前规则的名称。 |
rules[x].output |
这是当前规则的输出。 |
rules[x].output.type |
这是规则输出的类型。对于学术 API,这始终是 query。 |
rules[x].output.value |
这是规则的输出值。这将是查询表达式字符串。 |
根据前面的 JSON 输出创建 InterpretResponse 数据合约。我们感兴趣的是最后的数据字段,rules[x].output.value。这是查询表达式字符串,我们将用它来评估查询。
当 API 调用成功时,我们想要使用以下代码更新 ObservableCollection 类以反映可用的查询表达式:
ObservableCollection<string> tempList = new ObservableCollection<string>();
foreach (Interpretation interpretation in response.interpretations)
{
foreach (Rule rule in interpretation.rules) {
tempList.Add(rule.output.value);
}
}
AvailableQueryExpressions = tempList;
QueryExpression = AvailableQueryExpressions.FirstOrDefault();
我们遍历所有 interpretations,将规则中的 outputvalue 添加到我们的 AvailableQueryExpressions 中。
最后,我们将选定的 QueryExpression 设置为第一个可用的。这只是为了我们自己的方便。
成功的测试运行可以生成以下结果:

不成功的调用将产生错误响应代码。可以生成的响应代码如下:
| 响应代码 | 描述 |
|---|---|
400 |
错误的参数;请求参数缺失 |
401 |
无效的订阅密钥 |
403 |
调用量配额已超过 |
404 |
请求的资源未找到 |
500 |
内部服务器错误 |
在查询表达式中查找学术实体
现在我们有一个可用的查询表达式,我们可以使用 Evaluate 端点检索一组学术实体。这是一个 GET 请求,我们需要指定我们想要为每个实体返回的属性。我们将在稍后介绍可用的属性。
我们首先创建一个查询字符串,如下面的代码所示:
private async void Evaluate(object obj)
{
string queryString = $"expr={QueryExpression} &
attributes=Id,Ti,Y,D,CC,AA.AuN";
//queryString += "&model=latest";
//queryString += "&count=10";
//queryString += "&offset=0";5
//queryString += "&orderby=name:asc";
我们可以添加的参数如下表所述:
| 参数 | 描述 |
|---|---|
expr (必需) |
这是 Interpret 调用中找到的查询表达式。 |
attributes (可选) |
这是要包含在响应中的属性逗号分隔列表。每个属性都是大小写敏感的。 |
model (可选) |
这是要用于查询的模型。默认为最新模型。 |
count (可选) |
这是要返回的实体数量。 |
offset (可选) |
这是要返回的第一个结果的索引;它可以用于分页目的。 |
orderby (可选) |
这指定了排序实体的顺序。 |
注意,虽然 attributes 参数是可选的,但您应该指定您想要的属性。如果没有指定任何属性,则只返回实体 ID。
我们按照以下方式调用 API:
EvaluateResponse response = await _webRequest.MakeRequest<object,
EvaluateResponse>(HttpMethod.Get, $"evaluate?{queryString}");
if (response == null || response.entities.Length == 0)
return;
由于这是一个 GET 请求,我们不需要任何请求体。成功的调用将返回一个 EvaluateResponse 对象。这是一个数据合约,它将从 JSON 响应反序列化。
成功的响应将给出如下代码所示的 JSON 响应(取决于指定的属性):
{
"expr": "Composite(AA.AuN=='jaime teevan')",
"entities": [
{
"prob": 2.266e-007,
"Ti": "personalizing search via automated analysis of interests and
activities",
"Y": 2005,
"CC": 372,
"AA": [
{
"AuN": "jaime teevan",
"AuId": 1968481722
},
{
"AuN": "susan t dumais",
"AuId": 676500258
},
{
"AuN": "eric horvitz",
"AuId": 1470530979
}]
}]
}
响应包含我们使用的查询表达式。它还包含一个实体数组。数组中的每个项目将包含它正确的概率。它还将包含我们指定的所有属性,以字符串或数值形式呈现。它也可以是对象的形式,我们需要为这些对象提供数据合约。
对于我们的请求,我们指定了一些属性。这些是实体 ID、标题、出版年份和日期、引用次数和作者姓名。了解这些属性后,我们可以使用以下代码来输出结果:
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Expression {0} returned {1} entities\n\n", response.expr,
response.entities.Length);
foreach (Entity entity in response.entities)
{
sb.AppendFormat("Paper title: {0}\n\tDate: {1}\n", entity.Ti, entity.D);
sb.Append("Authors:\n");
foreach (AA author in entity.AA)
{
sb.AppendFormat("\t{0}\n", author.AuN);
}
sb.Append("\n");
}
Results = sb.ToString();
成功的调用可以给出以下输出:

任何错误响应都将产生响应代码,如前所述。
计算学术实体的属性分布
学术 API 的另一个功能是能够计算一组论文实体的属性值分布。这可以通过调用 calchistogram API 端点来完成。
这是一个 GET 请求,因此我们首先创建一个查询字符串,如下所示:
string queryString = $"expr={QueryExpression}&attributes=Y,F.FN";
//queryString += "&model=latest";
//queryString += "&count=10";
//queryString += "&offset=0";
我们可以指定的参数与 Evaluate 相同,除了我们没有 orderby 参数。对于这个调用,我们想要获取出版年份(Y)和研究领域的名称(F.FN)。
我们调用 API 时不指定任何请求体,如下面的代码所示:
HistogramResponse response = await _webRequest.MakeRequest<object,
HistogramResponse>(HttpMethod.Get, $"calchistogram?{queryString}");
if (response == null || response.histograms.Length == 0)
return;
如果调用成功,我们期望返回一个 HistogramResponse 对象。这是一个数据合约,它应该包含来自 JSON 响应的数据。
成功的请求应给出以下 JSON 响应(取决于请求的属性):
{
"expr": "And(Composite(AA.AuN=='jaime teevan'),Y>2012)",
"num_entities": 37,
"histograms": [
{
"attribute": "Y",
"distinct_values": 3,
"total_count": 37,
"histogram": [
{
"value": 2014,
"prob": 1.275e-07,
"count": 15
},
{
"value": 2013,
"prob": 1.184e-07,
"count": 12
},
{
"value": 2015,
"prob": 8.279e-08,
"count": 10
}]
},
{
"attribute": "F.FN",
"distinct_values": 34,
"total_count": 53,
"histogram": [
{
"value": "crowdsourcing",
"prob": 7.218e-08,
"count": 9
},
{
"value": "information retrieval",
"prob": 4.082e-08,
"count": 4
},
{
"value": "personalization",
"prob": 2.384e-08,
"count": 3
},
{
"value": "mobile search",
"prob": 2.119e-08,
"count": 2
}]
}]
}
响应包含我们使用的原始查询表达式。它将给出匹配实体的数量。还将出现一个直方图数组。这将包含我们请求的每个属性的项。每个项的数据在以下表格中描述:
| 数据字段 | 描述 |
|---|---|
attribute |
这是属性名称。 |
distinct_values |
这是匹配此属性实体的不同值的数量。 |
total_count |
这是匹配实体中此属性的所有值实例的总数。 |
histogram |
这是一个包含此属性直方图数据的数组。 |
histogram[x].value |
这是当前直方图的值。 |
histogram[x].prob |
这是匹配实体具有此属性值的概率。 |
histogram[x].count |
这是具有此值的匹配实体的数量。 |
成功响应后,我们遍历数据,使用以下代码在 UI 中展示:
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Totalt number of matching entities: {0}\n",
response.num_entities);
foreach (Histogram histogram in response.histograms)
{
sb.AppendFormat("Attribute: {0}\n", histogram.attribute);
foreach (HistogramY histogramY in histogram.histogram)
{
sb.AppendFormat("\tValue '{0}' was found {1} times\n", histogramY.value,
histogramY.count);
}
sb.Append("\n");
}
Results = sb.ToString();
成功的调用将返回以下结果:

不成功的 API 调用将返回一个错误,包含一个响应代码。可能的响应代码与之前关于Interpret功能的章节中描述的相同。
实体属性
API 的一个重要元素是属性的用法。你肯定希望从查询中获取一些数据,但不是全部数据。
我们之前已经看到如何在每个请求中指定属性。以下表格描述了所有可用的属性。请确保请求中指定的所有属性都正确无误:
| 属性 | 描述 |
|---|---|
Id |
实体 ID |
Ti |
论文标题 |
Y |
论文年份 |
D |
论文日期 |
CC |
引用计数 |
ECC |
估计引用计数 |
AA.AuN |
作者姓名 |
AA.AuId |
作者 ID |
AA.AfN |
作者所属机构名称 |
AA.AfId |
作者所属机构 ID |
F.FN |
研究领域名称 |
F.Fid |
研究领域 ID |
J.JN |
期刊名称 |
J.JId |
期刊 ID |
C.CN |
会议系列名称 |
C.Cid |
会议系列 ID |
Rid |
参考 ID |
W |
论文标题/摘要中的单词,用于全文搜索 |
E |
扩展元数据 |
扩展元数据在以下表格中描述:
| 属性 | 描述 |
|---|---|
DN |
论文显示名称 |
D |
描述 |
S |
来源(论文的网页来源,按静态排名排序) |
S.Ty |
来源类型(HTML/文本/PDF/DOC/PPT/XLS/PS) |
S.U |
来源 URL |
VFN |
场地全名 - 期刊或会议的全名 |
VSN |
场地简称 - 期刊或会议的简称 |
V |
期刊卷号 |
I |
期刊期号 |
FP |
论文第一页 |
LP |
论文最后一页 |
DOI |
数字对象标识符 |
使用知识探索服务创建后端
知识探索服务(KES)在某种程度上是学术 API 的后端。它允许我们从一个结构化数据中构建一个压缩索引,编写语法来解释自然语言。
要开始使用 KES,我们需要在本地安装服务。
注意
要下载 KES 安装程序,请访问 www.microsoft.com/en-us/download/details.aspx?id=51488。
安装过程中会附带一些示例数据,我们将使用这些数据。
要有一个工作的服务,所需的步骤如下:
-
定义一个 schema
-
生成数据
-
构建索引
-
编写语法
-
编译语法
-
托管服务
定义属性
schema 文件定义了我们领域中的属性结构。当我们之前讨论学术 API 时,我们看到了一系列不同的实体属性,我们可以通过查询检索这些属性。这些在 schema 中定义。
如果你打开 KES 安装的 Example 文件夹中的 Academic.schema 文件,你会看到定义的属性。我们有一个标题、年份和关键词,这些都是基本属性类型。此外,我们还有一个 Composite 属性用于作者。这个属性包含与作者相关的更多属性。
每个属性都将支持所有属性操作。可能存在不需要这种情况。为特定属性显式定义操作可能会减小索引大小。在作者 ID 的情况下,我们只想能够检查它是否等于某个值,这可以通过添加以下内容来实现:
{"name":"Author.Id", "type":"Int32", "operations":["equals"]}
添加数据
定义了 schema 之后,我们可以添加一些数据。示例中包含一个名为 Academic.data 的文件,它包含了所有示例数据。打开文件以了解数据可能的样子。
数据文件中的每一行指定了一个对象的属性值。它也可以包含一个 logprob 值,这将指示匹配对象的返回顺序。
构建索引
在有了属性 schema 和数据文件之后,我们可以构建压缩的二进制索引。这将包含我们所有的数据对象。
使用我们的示例文件,我们可以通过运行以下命令来构建索引:
kes.exe build_index Academic.schema Academic.data Academic.index
成功执行应生成 Academic.index 文件,我们将用它来托管服务。
当运行命令时,应用程序将连续输出状态,可能看起来如下:
00:00:00 Input Schema: \Programs\KES\Example\Academic.schema
00:00:00 Input Data: \Programs\KES\Example\Academic.data
00:00:00 Output Index: \Programs\KES\Example\Academic.index
00:00:00 Loading synonym file: Keyword.syn
00:00:00 Loaded 3700 synonyms (9.4 ms)
00:00:00 Pass 1 started
00:00:00 Total number of entities: 1000
00:00:00 Sorting entities
00:00:00 Pass 1 finished (14.5 ms)
00:00:00 Pass 2 started
00:00:00 Pass 2 finished (13.3 ms)
00:00:00 Processed attribute Title (20.0 ms)
00:00:00 Processed attribute Year (0.3 ms)
00:00:00 Processed attribute Author.Id (0.5 ms)
00:00:00 Processed attribute Author.Name (10.7 ms)
00:00:00 Processed attribute Author.Affiliation (2.3 ms)
00:00:00 Processed attribute Keyword (20.6 ms)
00:00:00 Pass 3 started
00:00:00 Pass 3 finished (15.5 ms, 73 page faults)
00:00:00 Post-processing started
00:00:00 Optimized attribute Title (0.1 ms)
00:00:00 Optimized attribute Year (0.0 ms)
00:00:00 Optimized attribute Author.Id (0.0 ms)
00:00:00 Optimized attribute Author.Name (0.5 ms)
00:00:00 Optimized attribute Author.Affiliation (0.2 ms)
00:00:00 Optimized attribute Keyword (0.6 ms)
00:00:00 Global optimization
00:00:00 Post-processing finished (17.2 ms)
00:00:00 Finalizing index
00:00:00 Total time: 157.6 ms
00:00:00 Peak memory usage: 23 MB (commit) + 0 MB (data file) = 23 MB
理解自然语言
在我们构建索引之后,我们可以开始创建我们的语法文件。这指定了服务可以理解的自然语言,以及它如何将其翻译成语义查询表达式。打开 academic.xml 文件,看看语法文件可以看起来像什么。
语法基于一个名为SRGS的W3C语音识别标准。顶级元素是语法元素。这需要一个root属性来指定根规则,它是语法的起点。
要允许属性引用,我们添加import元素。这需要是grammar元素的子元素,并且应该在其他任何内容之前。它包含两个必需的属性:要导入的方案文件名称,以及元素可以用来引用方案的名字。请注意,方案文件必须与语法文件在同一个文件夹中。
接下来是rule元素。这定义了一个结构单元,它指定了服务可以解释的查询表达式。rule元素需要一个id属性。可选地,你可以添加一个example元素,它用于描述可能被rule元素接受的短语。在这种情况下,这将是规则的子元素。
rule元素还包含一个item元素。这组成了一个语法构造的序列,可以用来指示序列的重复。或者,它可以用来指定替代方案,与 one-of 元素一起使用。
One-of 元素指定了项目元素之间的扩展。项目可以定义为 one-of 元素,其中“written by”和“authored by”作为扩展。
使用ruleref元素,我们可以通过使用更简单的规则来创建更复杂的表达式。它只是通过添加 URI 属性来引用其他规则。
attrref元素引用了index属性,这允许我们与索引中的属性进行匹配。属性 URI 是必需的,它必须指定要引用的索引模式和属性名称。这必须匹配通过import元素导入的模式。
tag元素定义了通过语法的路径。此元素允许你分配变量或执行函数以帮助语法的流程。
一旦语法文件完成,我们可以将其编译成二进制语法。这是通过运行以下命令来完成的:
kes.exe build_grammar Academic.xml Academic.grammar
运行此命令将生成类似以下内容的输出:
Input XML: \Programs\KES\Example\Academic.xml
Output Grammar: \Programs\KES\Example\Academic.grammar
本地托管和测试
在索引和语法就绪后,我们可以继续在本地测试服务。本地测试服务允许快速原型设计,这使我们能够快速定义方案和语法。
当我们在本地测试时,KES 仅支持最多 10,000 个对象和每秒 10 个请求。在执行了总共 1,000 个请求后,它也会终止。我们将在稍后学习如何绕过这些限制。
要本地托管 KES,请运行以下命令:
Kes.exe host_service Academic.grammar Academic.index -port 8080
这将启动在端口8080上运行的服务。为了验证它是否按预期工作,请打开您的浏览器并转到http://localhost:8080。
这样做应该会显示以下屏幕:

将 KES 作为本地服务运行还允许我们使用学术 API 进行测试。我们将对我们的示例应用程序进行一些修改——为学术 API 创建的——以支持这一点。
首先,我们将修改 WebRequest.cs 文件。我们需要确保我们可以更改端点,因此向类中添加以下函数:
public void SetEndpoint(string uri) {
_endpoint = uri;
}
接下来,我们需要向 MainView.xaml 文件中添加一个新的 TextBox 元素。这将允许我们输入一个 URL。这需要在 MainViewModel.cs 文件中有一个相应的字符串属性。当更改此属性时,我们需要在 _webRequest 对象上调用 SetEndpoint。这可以看起来如下所示:
private string _endpoint;
public string Endpoint {
get { return _endpoint; }
set {
_endpoint = value;
RaisePropertyChangedEvent("Endpoint");
_webRequest?.SetEndpoint(value);
}
}
最后,我们需要更新我们的 ViewModel 构造函数。将第一行更改为以下内容:
Endpoint = "https://api.projectoxford.ai/academic/v1.0/";
_webRequest = new WebRequest(Endpoint, "API_KEY_HERE");
这将使默认端点成为原始 API 地址,但允许我们使用应用程序在本地测试 KES。
通过使用本地端点测试应用程序,可以产生以下结果:

注意
注意,evaluate 和 calchistogram 将需要更新测试应用程序的请求中的属性,以便与本地 KES 一起工作。
寻求规模效应
虽然能够创建本地原型很令人高兴,但限制确保我们需要在其他地方部署服务以进行生产。在这种情况下,这意味着将 KES 部署到 Microsoft Azure。
我们现在将查看将 KES 部署到 Microsoft Azure 所需的步骤。
连接到 Microsoft Azure
第一步是下载 Azure 发布设置 文件。这个文件需要保存为 AzurePublishSettings.xml 并存储在 kes.exe 运行的目录中。
注意
你可以在 manage.windowsazure.com/publishsettings/ 找到 Azure 发布设置文件。
有两种方式可以在没有限制的情况下构建和托管 KES。第一种方式是在 Azure 中启动一个 Windows 虚拟机。在这个虚拟机上,你应该遵循我们在本地采取的相同步骤。这允许快速原型设计,但没有任何限制。
第二种方式是在本地运行 kes.exe,但添加 --remote 作为参数。这将创建一个临时的 Azure VM,构建索引,并将索引上传到指定的目标 blob 存储。一个示例命令可能如下所示:
kes.exe build_index
http://<account>.blob.core.windows.net/<container>/Academic.schema http://<account>.blob.core.windows.net/<container>/Academic.full.data http://<account>.blob.core.windows.net/<container>/Academic.full.index
--remote Large
这个过程可能需要长达 10 分钟,因此理想情况下,原型设计应在本地进行,或通过 Azure VM 进行。
部署服务
在语法和索引就位以及原型设计完成后,我们可以将服务部署到 Microsoft Azure 云服务。
注意
要了解如何创建 Microsoft Azure 云服务,请访问 azure.microsoft.com/en-us/documentation/articles/cloud-services-how-to-create-deploy/。
要将服务部署到预发布槽位,请运行以下命令:
kes.exe deploy_service
http://<account>.blob.core.windows.net/<container>/Academic.grammar
http://<account>.blob.core.windows.net/<container>/Academic.index
<serviceName> large --slot Staging
这将允许我们在将服务部署到生产槽之前执行基本测试。测试完成后,我们可以通过再次运行相同的命令并将“生产”作为最后一个参数来将其部署到生产环境。
当服务部署后,我们可以在浏览器中访问 http://<serviceName>.cloudapp.net 来测试它。
使用 QnA Maker 回答 FAQs
QnA Maker 允许我们使用现有的常见问题(FAQs)来创建一个回答这些问题的机器人。我们可以从现有的 FAQs 中生成一个知识库,并从中训练一个模型。
要开始,请转到 qnamaker.ai。在右上角点击“登录”或注册。

从常见问题创建知识库
如果尚未创建任何服务,我们可以通过点击“创建知识库”标签来创建一个。这将显示以下屏幕,如下两个截图所示:


-
通过点击截图中的步骤 1中的蓝色按钮在 Microsoft Azure 中创建一个 QnA 服务。
-
将 QnA 服务连接到知识库。
-
为服务输入一个名称。
-
输入要使用的基线 FAQs。这可以是单个或多个 URL 的形式,或者是一个包含问答对的文件。在我们的例子中,我们将从 URL 生成知识库。
-
让其余的设置保持默认。
-
点击创建您的 KB。
注意
如果您没有可用的 FAQs,可以使用 www.microsoft.com/en-us/software-download/faq 从 Microsoft 获取。
一旦创建了知识库,您将被带到包含所有问答对的页面。如下截图所示:

在这个页面上,我们可以查看所有来自我们 FAQ 源的问答对。我们也可以通过点击添加 QnA 对来添加新的问答对。
训练模型
每次我们对知识库进行更改时,点击保存和训练都是明智的选择。这将确保我们的模型是最新的,包含最新的问答对。
一旦我们训练了模型,我们就可以对其进行测试。这可以通过点击右侧的测试按钮来完成。这将显示以下聊天窗口:

从这个聊天对话框中,我们可以测试一些或所有的问题来验证我们是否得到了正确的答案。我们也可以通过以不同的方式提问来改进模型。在某些情况下,这可能会给我们提供错误的答案。
如果我们得到了错误的答案,我们可以通过选择正确的答案来更改它。对于任何给定的问题,可能的答案将通过点击问题下方的“检查”按钮列出,按概率排序。选择正确答案并重新训练模型将确保在稍后再次提出相同问题时得到正确答案。
发布模型
一旦训练完成,就是时候发布服务了。我们可以在顶部菜单点击“发布”来完成这一操作。这样做会显示一个基本的 HTTP 请求,我们可以尝试,如下面的截图所示:

在前面的截图中,我们可以看到要使用的端点、所需的应用程序 ID、订阅密钥以及请求体中的示例问题。所有这些参数都是获取成功响应所必需的。
成功调用服务将提供以下 JSON 响应:
{ "Answer": "Sample response", "Score": "0" }
如果我们有一个使用此服务的应用程序,我们可以决定当分数低于某个阈值时不再使用答案。
通常,我们会使用不同种类的机器人来使用此服务。例如,我们可以将其添加到 Skype 机器人或 Slackbot 中,或者简单地将其集成到客户支持网站的聊天机器人中。
摘要
在本章中,我们学习了关于项目学术知识 API 和项目知识探索服务的内容。我们了解了如何解释自然语言查询以获取用于评估的查询表达式。通过这次评估,我们从微软学术图知识库中检索了学术论文。从那里,我们学习了如何设置知识探索服务本身,从定义模式一直到将其部署到微软 Azure 云服务。最后,我们学习了如何设置一个简单的问答制作服务。
在下一章中,我们将继续探讨搜索 API,学习如何利用必应提供的不同搜索 API。
第九章:添加专业搜索
上一章探讨了学术论文与期刊之间的关系,我们学习了如何搜索学术论文。本章将进入顶级 API 的最后一部分,即搜索。在本章中,我们将学习如何搜索网络内容。我们将看到如何使用特定关键词或类别搜索最新新闻。进一步地,我们将搜索图片和视频,并学习如何自动为最终用户建议搜索查询。在本章结束时,我们将介绍 Bing Visual Search,并了解如何通过使用 Bing Custom Search 创建定制的搜索体验。
在本章中,我们将学习以下主题:
-
如何搜索网页和文档
-
如何搜索新闻文章
-
如何搜索图片和视频
-
如何在应用程序中添加自动建议
-
如何根据安全搜索策略过滤搜索结果
使用智能家居应用程序进行网络搜索
Bing Web Search API 为我们提供了与我们在 bing.com/search 找到的类似搜索体验。它返回与任何查询相关的结果。
对此 API 的任何请求的响应将包含网页、图片、视频和新闻文章。在典型场景中,这是您用于这些搜索的 API。
注意,在实际场景中,所有请求都应该从服务器端应用程序发出,而不是从客户端,就像我们在示例中所做的那样。
注意
如果您还没有这样做,请前往 portal.azure.com 注册 Bing Web Search API。您可以在 azure.microsoft.com/en-us/services/cognitive-services/bing-web-search-api/ 了解更多关于 API 的信息。
准备应用程序进行网络搜索
在深入探讨网络搜索所需的技术细节之前,我们将准备我们的智能家居应用程序。
在 Views 文件夹中添加一个名为 BingSearchView.xaml 的新视图。至少,它应该包含两个 Combobox 元素,一个用于搜索类型,一个用于搜索过滤器。我们需要一个 TextBox 元素用于我们的搜索查询,以及一个 Button 元素来执行搜索。最后,我们需要一个 TextBox 元素来显示搜索结果。
为了配合搜索类型和搜索过滤器,我们需要在 Model 文件夹中添加一个名为 BingSearchTypes.cs 的新文件。添加以下两个 enum:
public enum BingSearchType {
Web, News, NewsCategory
}
public enum SafeSearch {
Strict, Moderate, Off
}
添加这一功能使我们能够同时使用 Bing Web Search 和 Bing News Search API。后者将在稍后讨论。第二个 enum,SafeSearch,也将在稍后进行更详细的讨论。
我们需要一个新视图模型。在 ViewModels 文件夹中添加一个名为 BingSearchViewModel.cs 的新文件。在这里,我们需要为我们的搜索查询和搜索结果添加两个 string 属性。我们还需要一个 BingSearchType 类型的属性来表示选定的搜索类型。还需要一个 SafeSearch 类型的属性来表示选定的安全搜索过滤器。还需要一个 ICommand 属性用于我们的按钮。
此外,我们还需要能够显示之前创建的 SafeSearch enums 的值。这可以通过添加以下属性来实现:
public IEnumerable<BingSearchType> AvailableSearchTypes {
get {
return Enum.GetValues (typeof(BingSearchType)).Cast<BingSearchType>();
}
}
public IEnumerable<SafeSearch> SafeSearchFilter {
get {
return Enum.GetValues(typeof(SafeSearch)).Cast<SafeSearch>();
}
}
我们从每个 enum 中获取所有值,并将它们作为 IEnumerable 返回。
在编写本文档时,没有任何搜索 API 有任何 NuGet 客户端包,因此我们需要自己进行网络请求。将我们在早期章节中使用的 WebRequest.cs 文件复制到 Model 文件夹中。将文件重命名为 BingWebRequest.cs 并将类重命名为 BingWebRequest。
由于所有 API 调用都是 GET 请求,我们可以稍微简化这个类。从构造函数中移除 URL 参数,并完全移除 _endpoint 成员。这样做可以简化 MakeRequest 函数,如下所示:
public async Task<TResponse> MakeRequest<TResponse>(string url) {
try {
var request = new HttpRequestMessage(HttpMethod.Get, url);
HttpResponseMessage response = await _httpClient.SendAsync(request);
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);
}
我们不需要请求体,并已移除 TRequest 和相应的代码。我们还硬编码了 HTTP 方法,并说明在调用函数时我们将指定完整的 URL 端点。函数的其余部分应保持不变。
注意
记得添加对 System.Web 和 System.Runtime.Serialization 的引用。
在此基础上,我们可以继续前进。在继续之前,请确保代码可以编译并执行。
网络搜索
要使用 Bing Web Search,我们需要创建一个新的类。在 Model 文件夹中添加一个名为 BingSearch.cs 的新文件。
我们需要添加一个 BingWebRequest 类型的成员,我们将在构造函数中创建它:
private BingWebRequest _webRequest;
public BingSearch() {
_webRequest = new BingWebRequest("API_KEY_HERE");
}
创建一个名为 SearchWeb 的新函数。这个函数应该接受两个参数,一个用于搜索查询的字符串和一个 SafeSearch 参数。该函数应标记为 async 并返回 Task<WebSearchResponse>。WebSearchResponse 是我们将要了解更多信息的数据合约:
public async Task<WebSearchResponse> SearchWeb(string query, SafeSearch safeSearch)
{
string endpoint = string.Format("{0}{1}&safeSearch={2} &count=5&mkt=en-US","https://api.cognitive.microsoft.com/bing/v7.0/search?q=", query, safeSearch.ToString());
首先,我们构建我们的端点,它指向网络搜索服务。我们确保指定查询 q、safeSearch 选择和区域 mkt。后两个将在本章中讨论。
唯一必需的参数是查询字符串。其长度不应超过 1,500 个字符。其他可选参数将在下表中描述:
| 参数 | 描述 |
|---|---|
responseFilter |
要包含在响应中的结果类型的逗号分隔列表。如果未指定,结果将包含所有类型。合法值包括 Computation、Images、News、RelatedSearches、SpellSuggestions、TimeZone、Videos 和 WebPages。 |
setLang |
用于指定用户界面字符串的语言的两字母代码。 |
textDecorations |
指定查询词是否在结果中突出显示。默认为 false。 |
textFormat |
要应用于显示字符串的格式类型。可以是原始的或 HTML,默认为原始。 |
除了这些参数之外,还有一些其他参数。然而,它们对所有搜索都是通用的,将在本章末尾讨论。
在端点就绪后,我们可以继续:
try {
WebSearchResponse response = await _webRequest.MakeRequest<WebSearchResponse>(endpoint);
return response;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
}
return null;
使用新构建的端点,我们在_webRequest对象上调用MakeRequest。我们将 API 密钥和端点作为此调用的参数,并期望得到一个WebSearchResponse对象作为响应。
WebSearchResponse是一个数据合约,我们通过反序列化 API 服务的 JSON 响应来获取。顶层对象将包含不同结果类型的对象。请查看名为BingSearchResponse.cs的文件中提供的代码示例,以获取完整的数据合约。
注意
要获取 Bing 网络搜索的完整响应对象列表,请访问msdn.microsoft.com/en-us/library/dn760794.aspx#searchresponse。
返回到BingSearchViewModel.cs文件,我们可以添加BingSearch作为成员。构造函数应如下所示:
public BingSearchViewModel() {
_bingSearch = new BingSearch();
SearchCommand = new DelegateCommand(Search, CanSearch);
}
CanSearch参数应返回 true,如果我们已经在搜索查询文本字段中输入了任何文本。Search目前应如下所示:
private async void Search(object obj) {
switch (SelectedSearchType) {
case BingSearchType.Web:
var webResponse = await _bingSearch.SearchWeb(SearchQuery, SelectedSafeSearchFilter);
ParseWebSearchResponse(webResponse as WebSearchResponse);
break;
default:
break;
}
}
我们在_bingSearch对象上调用SearchWeb函数,将SearchQuery和SelectedSafeSearchFilter属性作为参数传递。在成功响应后,我们将响应发送到新函数ParseWebSearch:
private void ParseWebSearchResponse(WebSearchResponse webSearchResponse) {
StringBuilder sb = new StringBuilder();
Webpages webPages = webSearchResponse.webPages;
foreach (WebValue website in webPages.value)
{
sb.AppendFormat("{0}\n", website.name);
sb.AppendFormat("URL: {0}\n", website.displayUrl);
sb.AppendFormat("About: {0}\n\n", website.snippet);
}
SearchResults = sb.ToString();
}
当我们解释网络搜索的结果时,我们对结果中的webPages感兴趣。对于每个网页,我们希望输出名称、显示 URL 和描述性摘要。
网络搜索的成功测试运行应向我们展示以下结果:

来自网络搜索的结果对象包含一个RankingResponse对象。这将标识结果通常如何在搜索网站上显示,按主线和侧边栏排序。在生产系统中,你应该始终旨在按RankingResponse指定的顺序显示结果。
这可以通过两种方式完成。一种是通过指定的 ID 字段对所有结果进行排名。另一种方式稍微复杂一些。它涉及根据答案类型和结果索引拆分结果。
除了我们迄今为止看到的查询之外,我们还可以查询计算(例如,2 + 2)、时区计算和相关搜索。这些查询将产生 JSON 响应,这与常规网络搜索略有不同。
获取新闻
使用 Bing 新闻搜索 API,我们可以以几种方式搜索新闻。我们可以使用此 API 的三个端点:
-
/news:根据类别获取顶级新闻文章 -
/news/search:根据搜索查询获取新闻文章 -
/news/trendingtopics:获取顶级趋势新闻主题
在我们的智能屋应用中,我们将添加前两个,而我们将只理论性地涵盖最后一个。
注意
如果您还没有这样做,请前往portal.azure.com注册 Bing 新闻搜索 API。
来自查询的新闻
基于查询的新闻搜索的大部分基础工作已经在网络搜索示例中完成。为了根据给定的查询搜索新闻,我们需要在BingSearch类中添加一个新函数。
打开BingSearch.cs文件并添加一个名为SearchNews的新函数。这个函数应该接受一个string和一个SafeSearch参数。该函数应标记为async,并返回一个Task<BingNewsResponse>对象:
public async Task<BingNewsResponse> SearchNews(string query, SafeSearch safeSearch)
{
string endpoint = string.Format("{0}{1}&safeSearch={2}&count=5&mkt=en-US","https://api.cognitive.microsoft.com/bing/v7.0/news/search?q=", query,safeSearch.ToString());
我们将构建一个由 URL、搜索查询和safeSearch参数组成的端点。注意我们如何指定市场,mkt,同时将count限制为 5。这两个参数将在本章中详细描述。
我们唯一需要的参数是查询字符串,q。除了为网络搜索描述的参数(setLang、textDecorations和textFormat)之外,我们还可以指定一个名为originalImg的参数。这是一个布尔值,如果设置为 true,将提供一个指向原始图片的 URL(对于文章中的任何图片)。如果设置为 false,默认值,将提供一个缩略图的 URL。
在设置好端点后,我们可以调用 API:
try {
BingNewsResponse response = await _webRequest.MakeRequest<BingNewsResponse>(endpoint);
return response;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
}
return null;
我们在_webRequest对象上调用MakeRequest,将端点作为参数传递。
成功的调用将导致一个 JSON 响应,我们将将其反序列化为一个BingNewsResponse对象。这个对象需要作为一个数据合同来创建。
BingNewsResponse对象将包含一个新闻文章数组。这个数组中的每个项目将包含文章名称、URL、图片、描述、发布日期等。
注意
要了解新闻文章数组中每个项目的详细信息,请访问msdn.microsoft.com/en-us/library/dn760793.aspx#newsarticle。
在此基础上,我们可以回到BingSearchViewModel.cs文件并修改Search函数。我们通过在switch语句中添加对BingSearchType.News的 case 来实现这一点:
case BingSearchType.News:
var newsResponse = await _bingSearch.SearchNews(SearchQuery, SelectedSafeSearchFilter);
ParseNewsResponse(newsResponse as BingNewsResponse);
break;
成功的响应将被解析并显示在 UI 上:
private void ParseNewsResponse(BingNewsResponse bingNewsResponse) {
StringBuilder sb = new StringBuilder();
foreach(Value news in bingNewsResponse.value) {
sb.AppendFormat("{0}\n", news.name);
sb.AppendFormat("Published: {0}\n", news.datePublished);
sb.AppendFormat("{0}\n\n", news.description);
}
SearchResults = sb.ToString();
}
我们主要对新闻文章的名称、发布日期和描述感兴趣。
这次的良好测试运行应该给我们以下结果:

来自类别的新闻
当我们想要获取特定类别的顶级文章时,我们将执行与常规新闻查询类似的程序。区别在于我们构建的端点。
让我们在BingSearch类中创建一个新函数,名为SearchNewsCategory:
public async Task<BingNewsResponse> SearchNewsCategory(string query)
{
string endpoint = string.Format("{0}{1}&mkt=en-US", "https://api.cognitive.microsoft.com/bing/v5.0/news?category=", query);
在这里,我们有一个类别参数,包含我们希望搜索的主题。这是一个可选参数。如果它是空的,我们将获取所有类别的顶级新闻文章。
对于这次搜索,我们可以指定两个不同的市场,en-GB 和 en-US。每个市场都附带一个当前支持的预定义类别列表:
注意
要获取支持的类别完整列表,请访问 msdn.microsoft.com/en-us/library/dn760793.aspx#categoriesbymarket。
try {
BingNewsResponse response = await _webRequest.MakeRequest<BingNewsResponse>(endpoint);
return response;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
}
return null;
使用新构建的端点,我们在 _webRequest 对象上调用 MakeRequest。这应该会产生与常规新闻查询相同的响应对象。在我们的 ViewModel 中,我们在 Search 函数中添加了对这种搜索类型的 case。有了响应,我们利用已经创建的 ParseNewsResponse 来获取我们想要的数据。
趋势新闻
趋势新闻的搜索仅适用于 en-US 和 zh-CN 市场。要执行此搜索,请向以下 URL 发出请求:api.cognitive.microsoft.com/bing/v7.0/news/trendingtopics。
此调用不需要任何参数,但您可以添加过滤器,例如我们稍后将要讨论的常用过滤器。唯一的例外是 freshness 过滤器,它不会适用于此请求。
成功调用此端点将返回一个包含趋势话题数组的 TrendingTopicAnswer 对象。数组中的每个项目将包含以下数据:
| 数据字段 | 描述 |
|---|---|
image |
一个指向相关图片的链接 |
isBreakingNews |
一个布尔值,表示此主题是否被视为突发新闻 |
name |
主题的标题 |
query |
一个查询字符串,将返回此主题 |
webSearchUrl |
一个指向此主题 Bing 搜索结果的 URL |
webSearchUrlPingSuffix |
一个用于识别 webSearchUrl 的查询字符串片段 |
搜索图片和视频
Bing 图片搜索 API 和 Bing 视频搜索 API 允许我们直接搜索图片或视频。只有在您需要图片或视频内容时才应使用这些 API。有可能调用这些 API 会对性能和相关性产生负面影响,因此,我们应该尽量使用 Bing 网络搜索 API。
注意
如果您尚未这样做,请在 portal.azure.com 为 Bing 图片搜索 API 和 Bing 视频搜索 API 注册。
使用常见的用户界面
由于在我们的智能屋应用程序中不需要图片或视频搜索,我们将继续创建一个新的项目。使用我们在 第一章 中创建的 MVVM 模板来创建此项目,使用 Microsoft 认知服务入门。
这些 API 没有提供任何客户端包。像我们之前做的那样,我们应该真正从服务器端应用程序而不是客户端应用程序中进行这些调用。在任何情况下,我们需要将智能屋应用程序中的 BingWebRequest.cs 文件复制到 Model 文件夹。确保更改命名空间。
注意
记得添加对System.Web和System.Runtime.Serialization的引用。
我们需要安装Newtonsoft.Json NuGet 包以使我们的反序列化工作。通过 NuGet 包管理器进行安装。
由于我们将输出一些结果作为文本,我们可以使用一个通用的用户界面。
打开MainView.xaml文件。添加两个TextBox元素,一个用于搜索查询,一个用于结果。我们需要一个ComboBox元素来选择搜索类型。最后,我们需要添加一个Button元素用于搜索命令。
在MainViewModel.xaml文件中,我们需要添加一个表示搜索类型的enum。在文件的底部添加以下内容,位于类下方:
public enum SearchType {
ImageSearch,
VideoSearch,
}
我们只对基于查询的图像和视频搜索感兴趣。除了这些搜索形式,这两个 API 还可以搜索趋势图像和视频。必应视频搜索 API 还允许我们获取任何已搜索视频的更多详细信息。
在MainViewModel类中,我们需要添加两个与我们的TextBox元素对应的string属性。我们还需要一个类型为SearchType的属性来指示选定的搜索类型。为了指示我们有哪些可用的搜索类型,我们添加一个IEnumerable属性,如下所示:
public IEnumerable<SearchType> SearchTypes {
get {
return Enum.GetValues(typeof(SearchType)).Cast<SearchType>();
}
}
我们需要添加到我们的 ViewModel 中的最后一个属性是ICommand属性,它将被绑定到我们的Button元素。
现在,我们需要创建一个新的类,因此创建一个名为BingSearch.cs的新文件,位于Model文件夹中。这将负责构建正确的端点并执行两种搜索类型。
我们还需要添加一个类型为BingWebRequest的成员。这应该在构造函数中创建:
private BingWebRequest _webRequest;
public BingSearch() {
_webRequest = new BingWebRequest("API_KEY_HERE");
}
目前我们在这里需要做的就这些。
回到 ViewModel 中,我们需要添加一个类型为BingSearch的成员。有了这个,我们可以创建我们的构造函数:
public MainViewModel() {
_bingSearch = new BingSearch();
SearchCommand = new DelegateCommand(Search);
SelectedSearchType = SearchTypes.FirstOrDefault();
}
在 ViewModel 就绪后,我们可以进行一些搜索。
搜索图像
对于我们的示例,我们只将执行基于用户查询的图像搜索。为了允许这样做,我们需要在BingSearch类中添加一个函数。将函数命名为SearchImages,并让它接受一个字符串作为参数。该函数应返回Task<ImageSearchResponse>并标记为async。在这种情况下,ImageSearchResponse将是一个数据合同对象,数据是从我们的响应反序列化而来的:
public async Task<ImageSearchResponse> SearchImages(string query)
{
string endpoint = string.Format("{0}{1}","https://api.cognitive.microsoft.com/bing/v5.0/images/search?q=", query);
我们将首先构建我们的端点。在这种情况下,我们只指定查询参数,q。这是一个必需的参数。
除了我们即将看到的通用查询参数之外,我们还可以添加以下参数:
| 参数 | 描述 |
|---|---|
cab |
裁剪区域的底部坐标,取值范围从 0.0 到 1.0。从左上角开始测量。 |
cal |
裁剪区域的左坐标,取值范围从 0.0 到 1.0。 |
car |
裁剪区域的右坐标,取值范围从 0.0 到 1.0。 |
cat |
裁剪区域的顶部坐标,取值范围从 0.0 到 1.0。 |
ct |
要使用的裁剪类型。目前,唯一合法的值是 0 - 矩形。 |
此外,我们可以指定以下参数作为过滤器:
| 过滤名称 | 描述 |
|---|---|
aspect |
通过宽高比过滤图像。合法值有 Square、Wide、Tall 和 All。 |
color |
通过特定颜色过滤图像。 |
imageContent |
通过图像内容过滤图像。合法值有 Face 和 Portrait。 |
imageType |
通过图像类型过滤图像。合法值有 AnimatedGif、Clipart、Line、Photo 和 Shopping。 |
license |
通过适用于图像的许可证过滤图像。合法值有 Public、Share、ShareCommercially、Modify、ModifyCommercially 和 All。 |
size |
通过大小过滤图像。合法值有 Small(< 200 x 200 像素)、Medium(200 x 200 到 500 x 500 像素)、Large(>500 x 500 像素)、Wallpaper 和 All。 |
height |
仅获取具有特定高度的图像。 |
width |
仅获取具有特定宽度的结果。 |
确定了端点后,我们可以执行请求:
try {
ImageSearchResponse response = await _webRequest.MakeRequest<ImageSearchResponse>(endpoint);
return response;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
}
return null;
我们将在 _webRequest 对象上调用 MakeRequest,并将端点作为参数传递。成功的调用将导致 ImageSearchResponse,这是从 JSON 响应反序列化的数据合同对象。
结果对象将包含大量数据。其中之一是一个包含图像信息的数组。该数组中的每个项目都包含数据,例如图像名称、发布日期、URL 和图像 ID。
注意
要查看响应中可用的完整数据列表,请访问 msdn.microsoft.com/en-us/library/dn760791.aspx#images。
转到 MainViewModel.cs,我们现在可以创建 Search 函数:
private async void Search(object obj) {
SearchResult = string.Empty;
switch(SelectedSearchType) {
case SearchType.ImageSearch:
var imageResponse = await _bingSearch.SearchImages(SearchQuery);
ParseImageResponse(imageResponse);
break;
default:
break;
}
}
在成功响应后,我们将解析 imageResponse。通常,这意味着在列表或类似的地方显示图像,但我们将选择更简单的方法,通过输出文本信息:
private void ParseImageResponse(ImageSearchResponse imageResponse)
{
StringBuilder sb = new StringBuilder();
sb.Append("Image search results:\n\n");
sb.AppendFormat("# of results: {0}\n\n", imageResponse.totalEstimatedMatches);
foreach (Value image in imageResponse.value) {
sb.AppendFormat("\tImage name: {0}\n\tImage size: {1}\n\tImage host: {2}\n\tImage URL:{3}\t\n\n", image.name, image.contentSize, image.hostPageDisplayUrl, image.contentUrl);
}
SearchResult = sb.ToString();
}
我们将打印出搜索结果的数量。然后,我们将遍历图像数组,打印出每张图像的名称、大小、主机和 URL。
成功的测试运行应显示以下屏幕:

除了基于查询的图像搜索外,我们还可以搜索趋势图像。为此,您必须调用以下端点:api.cognitive.microsoft.com/bing/v7.0/images/trending。
目前,此功能仅适用于以下市场:en-US、en-CA 和 en-AU。对端点的成功调用将导致一个包含类别的数组。该数组中的每个项目将包含一个趋势图像数组以及类别的标题。
搜索视频
搜索视频的过程几乎与搜索图像相同。唯一的真正区别是我们如何构建端点以及我们得到的响应。
我们将在BingSearch类中添加一个新功能,以配合视频搜索:
public async Task<VideoSearchResponse> SearchVideos(string query)
{
string endpoint = string.Format("{0}{1}", "https://api.cognitive.microsoft.com/bing/v7.0/videos/search?q=", query);
如您所见,只有一个必需的参数:查询字符串,q。我们还可以指定一些对所有搜索 API 都通用的可选参数,这些参数将在后面进行描述。
除了常见的筛选器之外,视频还可以根据以下筛选器进行筛选:
| 筛选器 | 描述 |
|---|---|
pricing |
通过价格筛选视频。合法值是 Free、Paid 和 All。 |
resolution |
通过分辨率进行筛选。合法值是 480p、720p、1080p 和 All。 |
videoLength |
通过长度筛选视频。合法值是Short(< 5 分钟)、Medium(5 到 20 分钟)、Long(> 20 分钟)和All。 |
在端点就绪后,我们调用 API:
try {
VideoSearchResponse response = await _webRequest.MakeRequest<VideoSearchResponse>(endpoint);
return response;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
}
return null;
我们将在_webRequest对象上调用MakeRequest,并将端点作为参数传递。成功的调用将产生一个VideoSearchResponse对象。这是一个数据合约,从 JSON 响应反序列化而来。
除了其他数据外,它将包含一个视频数组。数组中的每个项目都包含视频名称、描述、发布者、时长、URL 等更多信息。
注意
要获取搜索响应中可用的完整数据列表,请访问msdn.microsoft.com/en-US/library/dn760795.aspx#videos。
为了能够搜索视频,我们在MainViewModel的Search函数中添加了一个新情况:
case SearchType.VideoSearch:
var videoResponse = await _bingSearch.SearchVideos(SearchQuery);
ParseVideoResponse(videoResponse);
break;
我们调用新创建的SearchVideos,并将搜索查询作为参数传递。如果调用成功,我们将继续解析视频:
private void ParseVideoResponse(VideoSearchResponse videoResponse)
{
StringBuilder sb = new StringBuilder();
sb.Append("Video search results:\n\n");
sb.AppendFormat("# of results: {0}\n\n",
videoResponse.totalEstimatedMatches);
foreach (VideoValue video in videoResponse.value) {
sb.AppendFormat("\tVideo name: {0}\n\tVideo duration: {1}\n\tVideo URL: {2}\t\n",video.name, video.duration, video.contentUrl);
foreach(Publisher publisher in video.publisher) {
sb.AppendFormat("\tPublisher: {0}\n", publisher.name);
}
sb.Append("\n");
}
SearchResult = sb.ToString();
}
至于图像,我们只是以文本形式显示视频信息。在我们的示例中,我们选择显示视频名称、时长、URL 和视频的所有发布者。
成功的视频搜索应该给出以下结果:

除了基于查询的视频搜索之外,我们还可以搜索热门视频。为此,您需要调用以下端点:api.cognitive.microsoft.com/bing/v7.0/videos/trending。
目前,此功能仅适用于以下市场:en-US、en-CA和en-AU。成功调用此端点将产生一个包含类别和图标的数组。类别数组中的每个项目将包含一个标题和子类别的数组。每个子类别将包含图标的数组和标题。图标数组中的每个项目将包含视频缩略图和用于获取特定视频的查询。
如果我们想获取任何视频的更多信息,我们可以查询以下端点:api.cognitive.microsoft.com/bing/v7.0/videos/details。
这需要我们指定一个id,以便我们可以识别视频。我们还可以指定modulesRequested。这是一个以逗号分隔的我们想要获取的详细信息列表。目前,合法值是All、RelatedVideos和VideoResult。
注意
要获取详细查询响应中可用的完整数据列表,请访问msdn.microsoft.com/en-US/library/dn760795.aspx#video。
使用自动建议帮助用户
自动建议是增强用户体验的绝佳方式。典型的用例是,每当用户在文本字段中输入一些文本时,都会显示一个建议词列表。
注意
如果您还没有这样做,请前往portal.azure.com注册 Bing Autosuggest API。
将自动建议添加到用户界面
由于 WPF 中的文本框不包含任何自动建议功能,我们需要自己添加一些。我们将使用第三方包,因此通过 NuGet 包管理器在我们的示例项目中安装WPFTextBoxAutoComplete包。
在MainView.xaml文件中,将以下属性添加到起始Window标签:
我们还需要确保当用户输入数据时,我们的搜索查询的TextBox绑定会更新。这可以通过确保Text属性看起来如下完成:
Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}"
在相同的TextBox元素中,添加以下内容:
behaviors:AutoCompleteBehavior.AutoCompleteItemsSource = "{Binding Suggestions}"
在 ViewModel 中,在MainViewModel.cs文件中,我们需要相应的属性。这应该是一个IEnumerable<string>对象。它将使用我们即将执行的自动建议查询的结果进行更新。
建议查询
要获取自动建议,我们首先添加一个新的类。在Model文件夹中添加一个名为BingAutoSuggest.cs的新文件。BingAutoSuggest类应该有一个类型为BingWebRequest的成员,该成员应在构造函数中创建。
创建一个名为Suggest的新函数。这个函数应该接受一个string作为参数,返回一个Task<List<string>>对象。将函数标记为async。
我们将首先构建一个端点,其中我们指定查询字符串q。此字段是必需的。我们还指定市场mkt,尽管这不是必需的。我们不需要任何其他参数。在我们执行 API 调用之前,我们将创建一个建议列表,并将其返回给调用者:
public async Task<List<string>> Suggest(string query) {
string endpoint = string.Format("{0}{1}&mkt=en-US", "https://api.cognitive.microsoft.com/bing/v7.0/suggestions/?q=", query);
List<string> suggestionResult = new List<string>();
我们将在_webRequest对象上调用MakeRequest,并将端点作为参数传递。如果调用成功,我们期望 JSON 响应反序列化为BingAutoSuggestResponse对象。此对象将包含一个suggestionGroups数组,其中每个项目包含一个SearchSuggestions数组。
SearchSuggestion的每个项目都包含一个 URL、显示文本、查询字符串和搜索类型。我们感兴趣的是显示文本,我们将其添加到suggestionResult列表中。此列表返回给调用者:
try {
BingAutoSuggestResponse response = await _webRequest.MakeRequest<BingAutoSuggestResponse>(endpoint);
if (response == null || response.suggestionGroups.Length == 0)
return suggestionResult;
foreach(Suggestiongroup suggestionGroup in response.suggestionGroups) {
foreach(Searchsuggestion suggestion in suggestionGroup.searchSuggestions) {
suggestionResult.Add(suggestion.displayText);
}
}
}
catch(Exception ex) {
Debug.WriteLine(ex.Message);
}
return suggestionResult;
注意
要获取响应数据的完整描述,请访问msdn.microsoft.com/en-us/library/mt711395.aspx#suggestions。
在MainViewModel.cs文件中,我们希望在键入时获取建议。我们将创建一个新函数,如下所示:
private async void GetAutosuggestions() {
var results = await _autoSuggest.Suggest(SearchQuery);
if (results == null || results.Count == 0) return;
Suggestions = results;
}
这将调用新创建的Suggest函数,并使用当前的SearchQuery值。如果有任何结果返回,我们将它们分配给之前创建的SuggestionsIEnumerable。确保在设置SearchQuery属性值时调用此函数。
在 UI 中,这将在搜索查询字段中自动填充第一个建议。这对用户来说并不理想,但对我们测试示例来说是可以的。
搜索共性
对于我们已涵盖的所有 API,有一些相似之处。我们现在将介绍这些。
语言
强烈建议指定你想要结果的市场。搜索通常会根据用户的当前位置返回本地市场和国家/地区的语言结果。正如你所想象的那样,这并不总是用户想要的。通过指定市场,你可以为用户定制搜索结果。
你如何从技术上解决这个问题取决于你应用程序的需求。对于智能家居应用程序,你可能允许用户在设置中设置市场。对于仅针对法国法国用户的网络应用程序,你可能不会允许用户更改市场。
通过在 GET 请求中添加mkt参数来指定市场。然后应指定市场代码,例如,en-US代表美国的英语。
备注
虽然任何 API 都可能支持特定市场,但某些功能可能不支持给定市场。
支持的语言子集包括英语、西班牙语、德语、荷兰语、法语、葡萄牙语、繁体中文、意大利语、俄语和阿拉伯语。
此外,我们可以在 GET 请求中指定一个cc参数。这指定了一个国家(通常是用户所在的国家)。此参数应采用两位字母国家代码的形式,例如,GB 代表英国。
可以指定广泛的国家的列表,并且这个列表会持续变化。
分页
一些搜索可能会产生大量结果。在这些情况下,你可能想要执行分页。这可以通过在 GET 请求中指定count和offset参数来实现。
如果你希望每页有 10 个结果,你将首先将计数设置为 10,并将第一页的偏移量设置为 0。当用户导航到下一页时,你将保持count为 10,但将偏移量增加到 10。对于下一页,你将偏移量增加到 20,依此类推。
每个查询返回的结果最大数量(计数参数)因 API 而异。请参阅以下表格以获取每个 API 当前的最大计数:
| API | 最大搜索结果 | 默认搜索结果 |
|---|---|---|
| 必应新闻搜索 | 100 | 10 |
| 必应网络搜索 | 50 | 10 |
| 必应图片搜索 | 150 | 35 |
| 必应视频搜索 | 105 | 35 |
筛选器
我们已经看到了一些针对单个 API 的筛选器。除此之外,还有一些可以应用于所有搜索的筛选器。
安全搜索
可以使用安全搜索过滤器来过滤搜索结果中的成人内容。此参数添加在请求 URL 中。
safeSearch 参数可以是以下值之一:
-
关闭: 返回所有结果项
-
适度: 结果项可以包含成人文本,但不会包含成人图像或视频
-
严格: 结果项中不包含任何成人文本、图像或视频
注意,如果用户的 IP 地址表明需要严格的搜索安全设置,则此设置将被忽略。在这种情况下,Bing 将默认使用严格的策略。
如果未设置参数,则默认为适度。
Freshness
通过向请求中添加 freshness 参数,你可以根据结果项的年龄过滤搜索结果。可以指定的值如下:
-
Day: 最后 24 小时的结果
-
Week: 最后 7 天的结果
-
Month: 最后 30 天的结果
错误
在我们涵盖的所有 API 中,对于每个请求,你可能都会收到一些可能的响应代码。以下表格描述了所有可能的响应代码:
| 代码 | 描述 |
|---|---|
200 |
请求成功。 |
400 |
缺少一个或多个必需的查询参数,或者其中一个参数无效。更多详细信息请参阅 ErrorResponse 字段。 |
401 |
提供的订阅密钥无效或缺失。 |
403 |
通常在月度配额超出时返回。如果调用者没有权限访问请求的资源,也可以使用。 |
410 |
使用了 HTTP 协议而不是仅支持的 HTTPS 协议。 |
429 |
每秒配额已超出。 |
使用 Bing Visual Search 搜索视觉内容
使用 Bing Visual Search API,可以解析图像。此 API 使我们能够深入了解图像。这包括查找视觉上相似的图像、搜索和购物来源。它还可以识别人物、地点和物体,以及文本。
发送请求
你通常会上传一个图像到 API 以获取对其的洞察。此外,你还可以传递一个图像的 URL。
注意
应用于查询 Bing Visual Search API 的端点是 api.cognitive.microsoft.com/bing/v7.0/images/visualsearch。
在任何情况下,都可以添加以下查询参数:
-
cc: 应该来自的结果所在国家的两位字母语言代码。
-
mkt: 结果来源的市场。这应该始终指定。
-
safeSearch: 用于过滤成人内容的过滤器。可以是 关闭、适度 或 严格。
-
setLang: 用于用户界面字符串的语言,即两位字母的语言代码。
此外,还必须指定两个内容头。这些是Content-Type和Ocp-Apim-Subscription-Key。第一个必须设置为multipart/form-data;boundary={BOUNDARY}。后者必须指定 API 密钥。
注意
有关内容头的更多信息,请访问docs.microsoft.com/en-us/azure/cognitive-services/bing-visual-search/overview#content-form-types。
接收响应
一旦请求通过,将返回一个 JSON 对象作为响应。
此对象将包含两个对象:一个tags数组和一个image字符串。图像字符串是图像的洞察令牌。tags列表包含一个tag名称和一系列actions(洞察)。在这个上下文中,标签意味着类别。例如,如果图像中识别出演员,这个标签可能是演员。
每个动作或洞察都描述了图像中的某些内容。它可能描述图像中的文本或图像中发现的不同的产品。每个动作都包含一系列数据。
注意
要查看默认洞察的完整列表,请访问docs.microsoft.com/en-us/azure/cognitive-services/bing-visual-search/default-insights-tag。
添加自定义搜索
Bing 自定义搜索为您提供了将强大的定制搜索体验添加到您自己的应用程序的机会。它允许您针对您关心的特定主题进行搜索。
通过使用www.customsearch.ai/上的门户,您可以创建一个定制的网络视图。
典型工作流程
如果您想构建一个自定义搜索网页,以下步骤描述了典型的工作流程。
-
创建自定义搜索实例:这可以在上一节中链接的门户中完成。
-
添加活动条目:这是一个应包含在搜索结果中的网站列表。
-
添加阻止条目:这是一个应从搜索结果中排除的网站列表。
-
添加固定条目:如果任何搜索词应该将网站固定在搜索结果顶部,应在固定条目部分指定。
-
配置托管 UI:设置布局、颜色主题和其他托管 UI 选项。
-
发布搜索实例:发布自定义搜索实例。
消费搜索实例
有三种方式来使用自定义搜索实例。
第一个,也是最容易的选项,是集成一个 JavaScript 代码片段。一旦您发布了搜索实例,您将获得一个预配置的 JavaScript 代码片段,渲染托管 UI。这可以粘贴到您的现有网页中。这将渲染您网站上的搜索表单。
另一个选项是直接链接到自定义 HTML 网站。这是在 JavaScript 代码片段中使用的链接,但仅用于直接链接。
最后一个选项是直接从您自己的代码中使用 REST API。在这本书中,我们不会对此进行更深入的探讨。
摘要
在本章中,我们探讨了不同的必应搜索 API。我们首先了解了如何使用必应网络搜索 API 来搜索各种内容。接下来,我们根据查询字符串和类别找到了最新的新闻。然后,我们转向了图片和视频搜索。此外,我们还探讨了如何通过添加自动建议来增强用户体验。我们是通过使用必应自动建议 API 来做到这一点的。最后,我们简要介绍了必应视觉搜索和必应自定义搜索。
在下一章和最后一章中,我们将总结全文。我们将通过连接各个部分来完成我们的智能家居应用程序。我们还将展望未来的道路。
第十章。连接各个部分
上一章重点介绍了最后一个 API 伞形,涵盖了 Bing 搜索 API。在本章中,我们将连接各个部分。我们的智能家居应用程序目前可以利用几个 API,但主要是单独的。我们将了解如何连接 LUIS、图像分析、Bing 新闻搜索和 Bing 语音 API。我们还将探讨完成本书后您可以采取的下一步行动。
在本章中,我们将学习以下主题:
-
通过连接多个 API 使应用程序更智能
-
利用 Microsoft 认知服务的实际应用
-
下一步
完成我们的智能家居应用程序
到目前为止,我们看到了所有不同的 API,大多数都是作为单独的 API。智能家居应用程序背后的整个想法是同时利用几个 API。
在本章中,我们将向 LUIS 添加一个新的意图。这个意图是用来获取不同主题的最新新闻。
接下来,我们想要实际使用 Bing 新闻 API 来搜索新闻。我们将通过允许最终用户说出一个命令,使用 Bing 语音 API 将语音转换为文本来实现这一点。
当我们找到一篇新闻文章时,我们希望获取标题、发布日期和描述。如果文章有相应的图片,我们希望获取图片的描述。我们将通过添加计算机视觉 API 来完成这项工作。
在所有新闻文章信息就绪后,我们希望将这些信息读回给我们。我们将通过将文本转换为语音来实现这一点。
创建意图
让我们先添加我们的新意图。转到www.luis.ai,并使用在第四章中创建的凭据登录,让应用程序理解命令。从首页进入您的智能家居应用程序。
在我们开始创建意图之前,我们需要添加一个新的实体。由于我们希望能够在特定主题的新闻中获取更新,我们将添加一个NewsCategory实体,如下面的截图所示:

由于这个实体将独立工作,我们不需要任何子实体。
现在我们可以添加一个新的意图。转到左侧的意图,然后点击添加意图。这将打开意图创建对话框。为意图输入一个合适的名称,例如GetNews:

我们还需要添加一个示例命令:

添加五个或六个更多如何表达这个意图的示例。确保在继续之前训练模型。
您可以通过转到右侧的测试来验证测试模型。
更新代码
使用新的意图,我们可以开始更新智能家居应用程序。
从意图执行操作
我们需要做的第一步是添加一个包含意图的enum变量。在Model文件夹中创建一个名为LuisActions.cs的新文件,并将以下内容添加到其中:
public enum LuisActions {
None, GetRoomTemperature, SetRoomTemperature, GetNews
}
如果您定义了其他意图,请也添加它们。
这个enum将在以后使用,以确定触发时执行哪个动作。例如,如果我们要求获取最新的体育新闻,GetNews将被触发,然后继续检索新闻。
为了让我们自己更容易一些,我们将使用本章剩余部分的现有 LUIS 示例。另一种选择是将此添加到HomeView中,我们可以在那里持续监听用户的语音命令。
为了触发一个动作,我们需要打开LuisViewModel.cs文件。找到OnLuisUtteranceResultUpdated函数。让我们将其更新如下:
private void OnLuisUtteranceResultUpdated(object sender, LuisUtteranceResultEventArgs e)
{
Application.Current.Dispatcher.Invoke(async () => {
StringBuilder sb = new StringBuilder(ResultText);
_requiresResponse = e.RequiresReply;
sb.AppendFormat("Status: {0}\n", e.Status);
sb.AppendFormat("Summary: {0}\n\n", e.Message);
在这个时候,我们没有添加任何新内容。我们已经移除了实体的输出,因为我们不再需要它了。
如果我们发现触发了任何动作,我们想要做些事情。我们调用一个新的函数TriggerActionExecution,并将意图名称作为参数传递:
if (!string.IsNullOrEmpty(e.IntentName))
await TriggerActionExectution(e.IntentName, e.EntityName);
我们很快就会回到这个函数。
通过添加以下代码来完成OnLuisUtteranceResultUpdated:
ResultText = sb.ToString();
});
}
再次,你应该看到没有新功能。然而,我们确实移除了最后的else子句。我们不希望应用程序再对我们说话总结。
创建新的TriggerActionExecution函数。让它接受一个string作为参数,并让它返回一个Task。将函数标记为async:
private async Task TriggerActionExectution(string intentName) {
LuisActions action;
if (!Enum.TryParse(intentName, true, out action))
return;
在这里,我们解析actionName(意图名称)。如果没有定义动作,我们不会做任何其他事情。
定义了动作后,我们进入switch语句来决定要做什么。因为我们只对GetNews情况感兴趣,所以我们从其他选项中跳出:
switch(action) {
case LuisActions.GetRoomTemperature:
case LuisActions.SetRoomTemperature:
case LuisActions.None:
default:
break;
case LuisActions.GetNews:
break;
}
}
在继续之前,确保代码可以编译。
命令搜索新闻
接下来,我们需要修改Luis.cs文件。因为我们已经为新闻主题定义了一个实体,我们想要确保我们从 LUIS 响应中获取这个值。
向LuisUtteranceResultEventArgs添加一个新属性:
public string EntityName { get; set; }
这将允许我们在接收到新闻主题值时添加它。
我们需要添加这个值。在Luis类中定位ProcessResult。修改if检查,使其看起来如下:
if (!string.IsNullOrEmpty(result.TopScoringIntent.Name)) {
var intentName = result.TopScoringIntent.Name;
args.IntentName = intentName;
}
else {
args.IntentName = string.Empty;
}
if(result.Entities.Count > 0) {
var entity = result.Entities.First().Value;
if(entity.Count > 0) {
var entityName = entity.First().Value;
args.EntityName = entityName;
}
}
我们确保设置最高得分的意图名称,并将其作为参数传递给事件。我们还检查是否设置了任何实体,如果是,则传递第一个。在实际应用中,你可能还会检查其他实体。
返回到LuisViewModel.cs文件,我们现在可以处理这个新属性。让TriggerActionExecution方法接受第二个string参数。在调用函数时,我们可以添加以下参数:
await TriggerActionExectution(e.IntentName, e.EntityName);
为了能够搜索新闻,我们需要添加一个BingSearch类型的新成员。这是我们在上一章中创建的类:
private BingSearch _bingSearch;
在构造函数中创建对象。
现在我们可以创建一个新的函数,称为GetLatestNews。这个函数应该接受一个string作为参数,并返回Task。将函数标记为async:
private async Task GetLatestNews(string queryString)
{
BingNewsResponse news = await _bingSearch.SearchNews (queryString, SafeSearch.Moderate);
if (news.value == null || news.value.Length == 0)
return;
当这个函数被调用时,我们在新创建的_bingSearch对象上调用SearchNews。我们将queryString作为参数传递,这将是动作参数。我们还设置安全搜索为Moderate。
成功的 API 调用将返回一个BingNewsResponse对象,该对象将包含一系列新闻文章。我们不会深入讨论这个类,因为我们已经在第九章中讨论过,添加专业搜索。
如果没有找到新闻,我们直接从函数中返回。如果我们找到了新闻,我们会做以下操作:
await ParseNews(news.value[0]);
我们调用一个函数ParseNews,稍后我们会回到这个函数。我们传递第一篇新闻文章,这将被解析。理想情况下,我们会遍历所有结果,但就我们的情况而言,这已经足够说明问题。
ParseNews方法应该标记为async。它的返回类型应该是Task,并且接受一个类型为Value的参数:
private async Task ParseNews(Value newsArticle) {
string articleDescription = $"{newsArticle.name}, published {newsArticle.datePublished}. Description:
{newsArticle.description}. ";
await _ttsClient.SpeakAsync(articleDescription, CancellationToken.None);
}
我们创建一个包含标题、发布日期和新闻描述的字符串。使用这个字符串,我们在_ttsClient上调用SpeakAsync,让应用程序将信息读回给我们。
在这个函数就位后,我们可以执行动作。在TriggerActionExecuted中,从GetNews情况调用GetLatestNews。确保等待调用完成。
当应用程序编译完成后,我们可以进行测试运行:

自然地,图像的效果不如现实生活中好。如果有麦克风和扬声器或耳机连接,我们可以通过音频请求最新的新闻,并得到用音频读回的新闻。
描述新闻图像
新闻文章通常还带有相应的图像。作为我们已有的内容的补充,我们可以添加图像分析。
我们需要做的第一步是添加一个新的 NuGet 包。搜索Microsoft.ProjectOxford.Vision包,并使用NuGet 包管理器安装此包。
在LuisViewModel.cs文件中,添加以下新成员:
private IVisionServiceClient _visionClient;
这可以在构造函数中创建:
_visionClient = new VisionServiceClient("FACE_API_KEY", "ROOT_URI");
这个成员将是访问计算机视觉 API 的入口点。
我们希望在ParseNews函数中获取一个描述图像的字符串。我们可以通过添加一个新的函数来实现,这个函数叫做GetImageDescription。这个函数应该接受一个string类型的参数,它将是图像的 URL。该函数的返回类型应该是Task<string>,并且应该标记为async:
private async Task<string> GetImageDescription(string contentUrl)
{
try {
AnalysisResult imageAnalysisResult = await _visionClient.AnalyzeImageAsync(contentUrl, new List<VisualFeature>() { VisualFeature.Description });
在这个函数中,我们在_visionClient上调用AnalyzeImageAsync。我们想要图像描述,所以我们指定在VisualFeature列表中。如果调用成功,我们期望得到一个类型为AnalysisResult的对象。这个对象应该包含按正确性概率排序的图像描述。
如果我们没有获取到任何描述,我们返回none。如果我们有描述,我们返回第一个描述的文本:
if (imageAnalysisResult == null || imageAnalysisResult.Description?.Captions?.Length == 0)
return "none";
return imageAnalysisResult.Description.Captions.First().Text;
}
如果发生任何异常,我们将异常消息打印到调试控制台。我们还向调用者返回none:
catch(Exception ex) {
Debug.WriteLine(ex.Message);
return "none";
}
}
在 ParseNews 中,我们可以在函数顶部添加以下代码来获取图片描述:
string imageDescription = await GetImageDescription (newsArticle.image.thumbnail.contentUrl);
获取图片描述后,我们可以将 articleDescription 字符串修改为以下内容:
string articleDescription = $"{newsArticle.name}, published
{newsArticle.datePublished}. Description:
{newsArticle.description}. Corresponding image is
{imageDescription}";
运行应用程序并请求新闻时,现在也会描述任何图片。这就完成了我们的智能屋应用程序。
使用 Microsoft Cognitive Services 的真实应用案例
目前有一些应用案例正在使用 Microsoft Cognitive Services。我们将在下面查看其中的一些。
Uber
Uber 是一个创建用来匹配司机和寻找乘车人的应用程序。人们可以打开应用程序,请求乘车。附近的注册 Uber 司机可以接载请求乘车的人。乘车后,司机将通过应用程序获得报酬。
为了确保更安全的体验,司机的照片将被发送给乘客。这样,乘客可以放心,司机就是他们所说的那个人。这可能会引起问题,因为司机可能不会总是看起来像他们的照片。他们可能长出了胡须,或者剃掉了胡须,或者发生了类似的变化。
为了解决这个问题,Uber 决定添加一个新功能。每位司机在使用应用程序时都需要登录。这样做将定期要求他们拍照。然后,这张照片将被发送到面部 API 进行验证。如果验证失败,由于眼镜反光或其他类似原因,将要求司机移除此类物品。
根据 Uber 的说法,他们大约花费了 3 周时间将面部 API 集成到他们的系统中。
DutchCrafters
DutchCrafters 是一家美国公司,销售手工家具。他们确实有一个实体店,但更重要的是,他们有一个电子商务网站。该网站包含超过 10,000 种产品,每种产品都可以定制。
他们的网站转化率较低,为了尝试提高这一比率,他们使用了手动推荐。在每种产品上手动添加推荐产品相当耗时。在考虑他们的选项时,他们发现了来自 Microsoft Cognitive Services 的推荐 API。
他们已经依赖 REST API,因此实现推荐 API 很快。DutchCrafters 表示,他们总共花费了 5 天时间来实现所需的功能。
由于他们的网站已经使用 ASP.NET 构建,并在 IIS 上运行,他们决定将所有内容迁移到云端。这样做提高了他们的网站性能,并且随着推荐 API 的加入,他们的基础也得到了加强。
在撰写本文时,他们正在使用 你可能喜欢这个 功能,为每种产品推荐 10 件商品。他们还在考虑添加基于用户历史数据的实时推荐功能,我们已看到这是使用推荐 API 可行的。
实施推荐 API 的直接结果是转化率的提高。他们看到转化率提高了三倍,大约 15% 的销售额来自推荐产品。
CelebsLike.me
CelebsLike.me 是微软的一个网络应用程序。它主要是为了展示微软认知服务的一些功能。
该应用程序的目的是找到你的名人双胞胎。你可以上传照片,或者使用在线找到的,应用程序将匹配与类似名人相似的面孔。
该应用程序利用了 Bing 图像搜索 API、计算机视觉 API 和面部 API。它识别网络图像中的名人面孔。当有人上传自己的照片时,将使用面部特征来找到匹配的名人。
Pivothead
Pivothead 是一家与可穿戴技术合作的公司。他们将眼镜与高质量的摄像头结合,提供静态图像和视频。这些眼镜允许人们捕捉他们所看到的生动视角内容。Pivothead 目前在消费市场有客户,同时也在商业市场。
随着时间的推移,Pivothead 看到了不断增长的成功,但似乎无法创建一个帮助视障和/或盲人的设备。他们在技术上遇到了困难,因为机器学习本身可能相当复杂。当他们了解到微软认知服务时,他们能够实现突破。
如果有人戴着眼镜,他们可以沿着耳塞滑动手指。这将捕捉到人面前的东西的图像。眼镜利用了微软认知服务的五个 API。这些是计算机视觉、情感、面部、语音和 LUIS。
通过分析人面前的一切图像,眼镜佩戴者将通过耳塞接收到的图像描述。如果检测到有人,将检测并描述他们的性别、外貌、所做的事情、年龄和情绪。如果检测到文本,将会读给这个人听。
根据 Pivothead 的说法,他们花了大约三个月的时间开发这些眼镜的原型。他们也表示,如果他们全职工作,可以在三周内完成。
Zero Keyboard
Zero Keyboard 应用程序是由一家名为 Blucup 的芬兰公司创建的。该公司发现销售人员存在一个共同问题。他们希望销售人员能够在路上捕捉客户数据并生成潜在客户。
他们开始开发适用于 iOS、Android 和 Windows Phone 的应用程序来解决这个问题。该应用程序背后的想法是记录客户信息,然后自动存储在 客户关系管理 (CRM) 系统中。
在开发时,Microsoft Cognitive Services 出现了,Blucup 决定尝试一下。之前,他们尝试了几种开源的语音识别软件和图像分析软件。没有一种提供了所需的质量和功能。
使用计算机视觉 API,应用程序可以拍摄名片或身份徽章,并识别文本。这些数据直接上传到他们的 CRM 系统。通过使用语音 API,销售人员还可以为每个联系人录制语音备忘录。
Blucup 表示,Microsoft Cognitive Services 提供非常准确的数据。此外,他们能够快速实现所需的 API,因为从开发者的角度来看,API 是一个很好的匹配。
主题
从所有这些例子中,你可以看到,Microsoft Cognitive Services 提供了高质量的服务。它也易于实现,这在考虑新的 API 时很重要。
关于 API 的另一个优点是,你不需要是数据科学家就能使用它们。尽管 API 背后的技术很复杂,但我们作为开发者,不需要考虑它。我们可以专注于我们最擅长的事情。
从这里去哪里
到现在为止,你应该已经了解了 Microsoft Cognitive Services 的基础知识,足以开始构建你自己的应用程序。
一个自然的下一步是尝试不同的 API。APIs 持续改进和优化。查看 API 文档,跟上变化并了解更多是值得的。此外,Microsoft 还在不断向服务中添加新的 API。在撰写这本书的过程中,我看到了三个新的 API 被添加。这些可能值得深入研究。
另一个可能性是建立在我们在智能家居应用上开始的基础上。我们已经打下了一些基础,但还有很多机会。也许你可以改进我们已经拥有的东西。也许你能看到混合其他 API 的机会,我们之前已经介绍过。
阅读这本书可能会给你一些自己的想法。一个很好的前进方式是实施它们。
正如我们所看到的,有许多可能的应用 API 的区域。只有想象力限制了使用。
也许这本书激发了你对机器学习的更深层兴趣。我们迄今为止看到的一切都是机器学习。尽管它比仅仅使用 API 更复杂,但确实值得进一步探索。
摘要
通过这一章,我们完成了我们的旅程。我们为新闻检索创建了一个新的意图。我们学习了如何处理从这个意图触发的动作。基于语音命令,我们成功地获取了一个主题的最新新闻,并让智能家居应用程序读给我们听。接下来,我们继续了解今天有哪些现实生活中的应用正在使用 Microsoft Cognitive Services。最后,我们通过查看完成这本书后你可以采取的一些自然下一步来结束这一章。
附录 A. LUIS 实体
在本附录中,我们将列出 LUIS 中的预构建实体。
LUIS 预构建实体
以下列表显示了可以添加到您应用程序的所有可用实体:
-
DatetimeV2 -
Datetime -
Number -
Ordinal -
Percentage -
Temperature -
Dimension -
Money -
Age -
Geography -
Encyclopedia -
URL -
Email -
Phone Number
完整且更新的预构建实体列表可以在docs.microsoft.com/en-us/azure/cognitive-services/LUIS/pre-builtentities找到。
附录 B. 许可信息
本附录包含几个第三方库,它们有不同的许可协议。所有库及其适用的许可协议将在接下来的几页中介绍。
视频帧分析器
版权所有(c)微软。保留所有权利。
本软件受 MIT 许可协议许可。
微软认知服务: www.microsoft.com/cognitive
微软认知服务 GitHub: github.com/Microsoft/Cognitive
版权所有(c)微软公司
所有权利保留。
MIT 许可协议:
凡获得本软件及其相关文档副本(“软件”)的个人,在此免费授予在不限制使用、包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许向软件提供者授予此类权利,但受以下条件约束:
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何形式的保证,无论是明示的、暗示的,包括但不限于适销性、特定用途适用性和非侵权性保证。在任何情况下,作者或版权所有者不对任何索赔、损害或其他责任负责,无论这些责任是因合同、侵权或其他原因引起的,无论这些责任是否与软件或其使用或其他方式有关。
OpenCvSharp3
本许可协议也被称为新 BSD 许可或修改版 BSD 许可。另请参阅2 条款 BSD 许可。
在满足以下条件的情况下,允许以源代码和二进制形式分发和使用,无论是否修改:
-
源代码的再分发必须保留上述版权声明、本许可列表和以下免责声明。
-
二进制形式的再分发必须复制上述版权声明、本许可列表和以下免责声明在随分发提供的文档和其他材料中。
-
未经具体事先书面许可,不得使用版权所有者或其贡献者的名称来认可或推广源自本软件的产品。
本软件由版权所有者和贡献者按“原样”提供,并放弃任何明示或暗示的保证,包括但不限于适销性和特定用途适用性的暗示保证。在任何情况下,版权所有者或贡献者不对任何直接、间接、偶然、特殊、示范性或后果性损害(包括但不限于替代商品或服务的采购;使用、数据或利润的损失;或业务中断)承担责任,无论这些损害是否由本软件的使用或其使用方式引起,即使被告知了此类损害的可能性。
Newtonsoft.Json
MIT 许可证 (MIT)
版权所有 (c) 2007 詹姆斯·牛顿-金
以下是对任何获得本软件及其相关文档文件(“软件”)副本的个人免费授予许可,以无限制地处理软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许软件的接收者执行上述操作,前提是遵守以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何形式的保证,无论是明示的、暗示的,包括但不限于适销性、特定用途适用性和非侵权性保证。在任何情况下,作者或版权所有者不对任何索赔、损害或其他责任负责,无论这些责任是因合同、侵权或其他原因引起的,无论这些责任是否与软件或其使用或其他方式有关。
NAudio
微软公共许可证 (Ms-PL)
本许可证管理伴随软件的使用。如果你使用软件,则接受本许可证。如果你不接受本许可证,则不要使用软件。
定义
本许可证中的术语“复制”、“复制品”、“衍生作品”和“分发”在此处的含义与根据美国版权法相同。
贡献 是原始软件,或对软件的任何增加或更改。
贡献者 是任何在许可证下分发其贡献的个人。
授权专利 是贡献者的专利权要求,这些要求直接针对其贡献。
权利授予
(A) 版权许可 - 在本许可证的条款下,包括第三部分中的许可证条件和限制,每个贡献者授予你一个非独占的、全球性的、免版税的版权许可,以复制其贡献、制作其贡献的衍生作品,以及分发其贡献或你创建的任何衍生作品。
(B) 专利许可 - 在本许可证的条款下,包括第三部分中的许可条件和限制,每个贡献者授予您一项非独占的、全球性的、免版税的许可,根据其许可的专利,在软件或软件贡献的衍生作品中制造、制作、使用、销售、提供销售、进口以及/或以其他方式处置其贡献。
条件和限制
(A) 无商标许可:本许可证不授予您使用任何贡献者名称、标志或商标的权利。
(B) 如果您就您声称由软件侵犯的专利对任何贡献者提起专利权诉讼,您从该贡献者获得的软件专利许可将自动终止。
(C) 如果您分发软件的任何部分,您必须保留软件中现有的所有版权、专利、商标和归属通知。
(D) 如果您以源代码形式分发软件的任何部分,您只能通过包括本许可证的完整副本来这样做。如果您以编译或目标代码形式分发软件的任何部分,您只能在本许可证符合的许可证下这样做。
(E) 软件按原样许可。您使用软件的风险自负。贡献者不提供任何明确的保证、担保或条件。您可能根据当地法律享有额外的消费者权利,本许可证不能改变这些权利。在您当地法律允许的范围内,贡献者排除对适销性、特定用途适用性和非侵权的默示保证。







浙公网安备 33010602011771号